From e00cb482382886fad2f59ca5a51ba610d72cd83b Mon Sep 17 00:00:00 2001 From: Patryk Strusiewicz-Surmacki Date: Mon, 25 Mar 2024 18:26:48 +0100 Subject: [PATCH] Added support for gradual rollout Signed-off-by: Patryk Strusiewicz-Surmacki --- Dockerfile | 10 +- Makefile | 53 +- agent.Dockerfile | 37 ++ api/v1alpha1/networkconfigrevision_types.go | 102 ++++ api/v1alpha1/nodenetworkconfig_types.go | 80 +++ api/v1alpha1/zz_generated.deepcopy.go | 199 ++++++++ cmd/{manager => agent}/main.go | 89 ++-- cmd/operator/main.go | 202 ++++++++ .../manager.yaml => agent/agent.yaml} | 16 +- .../agent_master.yaml} | 8 +- config/{manager => agent}/config.yaml | 0 .../controller_agent_config.yaml} | 0 config/{manager => agent}/kustomization.yaml | 12 +- config/{manager => agent}/namespace.yaml | 0 config/{manager => agent}/service.yaml | 0 config/certmanager/kustomization.yaml | 3 + ...iff.telekom.de_networkconfigrevisions.yaml | 320 ++++++++++++ ....schiff.telekom.de_nodenetworkconfigs.yaml | 283 +++++++++++ config/crd/kustomization.yaml | 2 + config/default/kustomization.yaml | 3 +- config/default/manager_auth_proxy_patch.yaml | 2 +- config/default/manager_config_patch.yaml | 16 +- .../default/manager_master_config_patch.yaml | 14 +- .../default/manager_master_metrics_patch.yaml | 2 +- .../default/manager_master_webhook_patch.yaml | 2 +- config/default/manager_metrics_patch.yaml | 4 +- config/default/manager_webhook_patch.yaml | 4 +- config/operator/kustomization.yaml | 12 + config/operator/operator.yaml | 65 +++ config/rbac/role.yaml | 52 ++ ...ion_controller.go => config_controller.go} | 44 +- controllers/nodenetworkconfig_controller.go | 85 ++++ ...e_controller.go => revision_controller.go} | 34 +- .../vrfrouteconfiguration_controller.go | 63 --- frr-exporter.Dockerfile | 2 +- go.mod | 64 +-- go.sum | 178 +++---- pkg/frr/dbus/dbus.go | 2 +- .../dbus/mock/{mock_frr.go => mock_dbus.go} | 0 pkg/frr/manager.go | 15 + pkg/frr/mock/mock_frr.go | 122 +++++ pkg/healthcheck/healthcheck.go | 54 +- pkg/healthcheck/healthcheck_test.go | 50 +- pkg/managerconfig/managerconfig_test.go | 8 +- pkg/reconciler/config_reconciler.go | 196 ++++++++ pkg/reconciler/configrevision_reconciler.go | 471 ++++++++++++++++++ pkg/reconciler/layer2.go | 71 +-- pkg/reconciler/layer3.go | 48 +- .../nodenetworkconfig_reconciler.go | 282 +++++++++++ pkg/reconciler/reconciler.go | 130 ----- pkg/reconciler/reconciler_test.go | 385 ++++++++++++++ 51 files changed, 3294 insertions(+), 602 deletions(-) create mode 100644 agent.Dockerfile create mode 100644 api/v1alpha1/networkconfigrevision_types.go create mode 100644 api/v1alpha1/nodenetworkconfig_types.go rename cmd/{manager => agent}/main.go (83%) create mode 100644 cmd/operator/main.go rename config/{manager/manager.yaml => agent/agent.yaml} (92%) rename config/{manager/manager_master.yaml => agent/agent_master.yaml} (96%) rename config/{manager => agent}/config.yaml (100%) rename config/{manager/controller_manager_config.yaml => agent/controller_agent_config.yaml} (100%) rename config/{manager => agent}/kustomization.yaml (66%) rename config/{manager => agent}/namespace.yaml (100%) rename config/{manager => agent}/service.yaml (100%) create mode 100644 config/crd/bases/network.schiff.telekom.de_networkconfigrevisions.yaml create mode 100644 config/crd/bases/network.schiff.telekom.de_nodenetworkconfigs.yaml create mode 100644 config/operator/kustomization.yaml create mode 100644 config/operator/operator.yaml rename controllers/{layer2networkconfiguration_controller.go => config_controller.go} (58%) create mode 100644 controllers/nodenetworkconfig_controller.go rename controllers/{routingtable_controller.go => revision_controller.go} (55%) delete mode 100644 controllers/vrfrouteconfiguration_controller.go rename pkg/frr/dbus/mock/{mock_frr.go => mock_dbus.go} (100%) create mode 100644 pkg/frr/mock/mock_frr.go create mode 100644 pkg/reconciler/config_reconciler.go create mode 100644 pkg/reconciler/configrevision_reconciler.go create mode 100644 pkg/reconciler/nodenetworkconfig_reconciler.go delete mode 100644 pkg/reconciler/reconciler.go create mode 100644 pkg/reconciler/reconciler_test.go diff --git a/Dockerfile b/Dockerfile index 9b7f215f..f360cd78 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM docker.io/library/golang:1.21-alpine as builder +FROM docker.io/library/golang:1.21-alpine AS builder WORKDIR /workspace @@ -14,7 +14,7 @@ RUN go mod download RUN apk add llvm clang linux-headers libbpf-dev musl-dev # Copy the go source -COPY cmd/manager/main.go main.go +COPY cmd/operator/main.go main.go COPY api/ api/ COPY controllers/ controllers/ COPY pkg/ pkg/ @@ -25,14 +25,14 @@ RUN cd pkg/bpf/ && go generate ARG ldflags # Build -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "${ldflags}" -a -o manager main.go +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "${ldflags}" -a -o operator main.go FROM alpine:latest RUN apk add --no-cache iptables ip6tables WORKDIR / -COPY --from=builder /workspace/manager . +COPY --from=builder /workspace/operator . USER 65532:65532 -ENTRYPOINT ["/manager"] +ENTRYPOINT ["/operator"] diff --git a/Makefile b/Makefile index 5e012a44..9ceca459 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,10 @@ -# Image URL to use all building/pushing image targets -IMG ?= ghcr.io/telekom/das-schiff-network-operator:latest +# Agent image URL to use all building/pushing image targets +AGENT_IMG ?= ghcr.io/telekom/das-schiff-network-operator-agent:latest # Sidecar image URL to use all building/pushing image targets SIDECAR_IMG ?= ghcr.io/telekom/frr-exporter:latest +# Operator image URL to use all building/pushing image targets +OPERATOR_IMG ?= ghcr.io/telekom/das-schiff-network-opeator:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.25 @@ -41,7 +43,7 @@ all: build help: ## Display this help. @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) -##@ DevelopmentLDFLAGS := $(shell hack/version.sh) +##@ Development .PHONY: bpf-generate bpf-generate: ## Run go generate for the BPF program (builds it as well) @@ -70,8 +72,18 @@ test: manifests generate fmt vet envtest ## Run tests. ##@ Build .PHONY: build -build: generate fmt vet ## Build manager binary. - go build -ldflags "$(LDFLAGS)" -o bin/manager cmd/manager/main.go +build: generate fmt vet ## Build agent binary. + go build -ldflags "$(LDFLAGS)" -o bin/operator cmd/operator/main.go + go build -ldflags "$(LDFLAGS)" -o bin/agent cmd/agent/main.go + go build -ldflags "$(LDFLAGS)" -o bin/frr-exporter cmd/frr-exporter/main.go + +.PHONY: operator-build +operator-build: generate fmt vet ## Build agent binary. + go build -ldflags "$(LDFLAGS)" -o bin/operator cmd/operator/main.go + +.PHONY: agent-build +agent-build: generate fmt vet ## Build agent binary. + go build -ldflags "$(LDFLAGS)" -o bin/agent cmd/agent/main.go .PHONY: sidecar-build sidecar-build: build @@ -79,24 +91,40 @@ sidecar-build: build .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run -ldflags "$(LDFLAGS)" ./cmd/manager/main.go + go run -ldflags "$(LDFLAGS)" ./cmd/agent/main.go .PHONY: docker-build -docker-build: #test ## Build docker image with the manager. - docker build --build-arg ldflags="$(LDFLAGS)" -t ${IMG} . +docker-build: test ## Build docker image with the manager. + docker build --build-arg ldflags="$(LDFLAGS)" -t ${OPERATOR_IMG} . + docker build --build-arg ldflags="$(LDFLAGS)" -t ${AGENT_IMG} -f agent.Dockerfile . + docker build --build-arg ldflags="$(LDFLAGS)" -t ${SIDECAR_IMG} -f frr-exporter.Dockerfile . + +.PHONY: docker-build-agent +docker-build-agent: test ## Build docker image with the manager. + docker build --build-arg ldflags="$(LDFLAGS)" -t ${AGENT_IMG} -f agent.Dockerfile . .PHONY: docker-build-sidecar docker-build-sidecar: test ## Build docker image with the manager. docker build --build-arg ldflags="$(LDFLAGS)" -t ${SIDECAR_IMG} -f frr-exporter.Dockerfile . +.PHONY: docker-build-operator +docker-build-operator: test ## Build docker image with the manager. + docker build --build-arg ldflags="$(LDFLAGS)" -t ${OPERATOR_IMG} . + .PHONY: docker-push -docker-push: ## Push docker image with the manager. - docker push ${IMG} +docker-push: docker-push-agent docker-push-sidecar docker-push-operator + +.PHONY: docker-push-agent +docker-push-agent: ## Push docker image with the manager. + docker push ${AGENT_IMG} .PHONY: docker-push-sidecar docker-push-sidecar: ## Push docker image with the manager. docker push ${SIDECAR_IMG} +.PHONY: docker-push-operator +docker-push-operator: ## Push docker image with the manager. + docker push ${OPERATOR_IMG} ##@ Release @@ -135,8 +163,9 @@ uninstall-certs: manifests kustomize ## Uninstall certs .PHONY: deploy deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - cd config/manager && $(KUSTOMIZE) edit set image frr-exporter=${SIDECAR_IMG} + cd config/agent && $(KUSTOMIZE) edit set image agent=${AGENT_IMG} + cd config/agent && $(KUSTOMIZE) edit set image frr-exporter=${SIDECAR_IMG} + cd config/operator && $(KUSTOMIZE) edit set image operator=${OPERATOR_IMG} $(KUSTOMIZE) build config/default | kubectl apply -f - .PHONY: undeploy diff --git a/agent.Dockerfile b/agent.Dockerfile new file mode 100644 index 00000000..54774b12 --- /dev/null +++ b/agent.Dockerfile @@ -0,0 +1,37 @@ +# Build the manager binary +FROM docker.io/library/golang:1.21-alpine AS builder + + +WORKDIR /workspace +# Copy the Go Modules manifests +COPY go.mod go.mod +COPY go.sum go.sum +# cache deps before building and copying source so that we don't need to re-download as much +# and so that source changes don't invalidate our downloaded layer +RUN go mod download + +# Build router +RUN apk add llvm clang linux-headers libbpf-dev musl-dev + +# Copy the go source +COPY cmd/agent/main.go main.go +COPY api/ api/ +COPY controllers/ controllers/ +COPY pkg/ pkg/ + +# Build router +COPY bpf/ bpf/ +RUN cd pkg/bpf/ && go generate + +# Build +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o agent main.go + +FROM alpine:latest + +RUN apk add --no-cache iptables ip6tables + +WORKDIR / +COPY --from=builder /workspace/agent . +USER 65532:65532 + +ENTRYPOINT ["/agent"] diff --git a/api/v1alpha1/networkconfigrevision_types.go b/api/v1alpha1/networkconfigrevision_types.go new file mode 100644 index 00000000..905db700 --- /dev/null +++ b/api/v1alpha1/networkconfigrevision_types.go @@ -0,0 +1,102 @@ +/* +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 v1alpha1 + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NetworkConfigSpec defines the desired state of NetworkConfig. +type NetworkConfigRevisionSpec struct { + // Config stores global configuration of the nodes. + Config NodeNetworkConfigSpec `json:"config"` + // Revision is a hash of the NetworkConfigRevision object that is used to identify the particular revision. + Revision string `json:"revision"` +} + +type NetworkConfigRevisionStatus struct { + // IsInvalid determines if NetworkConfigRevision results in misconfigured nodes (invalid configuration). + IsInvalid bool `json:"isInvalid"` + // Ready informs about how many nodes were already provisioned with a config derived from the revision. + Ready int `json:"ready"` + // Ongoing informs about how many nodes are currently provisioned with a config derived from the revision. + Ongoing int `json:"ongoing"` + // Queued informs about how many nodes are currently waiting to be provisiined with a config derived from the revision. + Queued int `json:"queued"` + // Total informs about how many nodes in total can be provisiined with a config derived from the revision. + Total int `json:"total"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:shortName=ncr,scope=Cluster +//+kubebuilder:printcolumn:name="Invalid",type=string,JSONPath=`.status.isInvalid` +//+kubebuilder:printcolumn:name="Queued",type="integer",JSONPath=".status.queued" +//+kubebuilder:printcolumn:name="Ongoing",type="integer",JSONPath=".status.ongoing" +//+kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.ready" +//+kubebuilder:printcolumn:name="Total",type="integer",JSONPath=".status.total" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// NetworkConfigRevision is the Schema for the node configuration. +type NetworkConfigRevision struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NetworkConfigRevisionSpec `json:"spec,omitempty"` + Status NetworkConfigRevisionStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// NetworkConfigRevisionList contains a list of NetworkConfigRevision. +type NetworkConfigRevisionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NetworkConfigRevision `json:"items"` +} + +func NewRevision(config *NodeNetworkConfig) (*NetworkConfigRevision, error) { + data, err := json.Marshal(config.Spec) + if err != nil { + return nil, fmt.Errorf("error marshalling data: %w", err) + } + + h := sha256.New() + if _, err := h.Write(data); err != nil { + return nil, fmt.Errorf("failed hashing network config: %w", err) + } + hash := h.Sum(nil) + hashHex := hex.EncodeToString(hash) + + return &NetworkConfigRevision{ + ObjectMeta: metav1.ObjectMeta{Name: hashHex[:10]}, + Spec: NetworkConfigRevisionSpec{ + Config: config.Spec, + Revision: hashHex, + }, + Status: NetworkConfigRevisionStatus{}, + }, nil +} + +func init() { + SchemeBuilder.Register(&NetworkConfigRevision{}, &NetworkConfigRevisionList{}) +} diff --git a/api/v1alpha1/nodenetworkconfig_types.go b/api/v1alpha1/nodenetworkconfig_types.go new file mode 100644 index 00000000..2fe85bba --- /dev/null +++ b/api/v1alpha1/nodenetworkconfig_types.go @@ -0,0 +1,80 @@ +/* +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// NodeNetworkConfigSpec defines the desired state of NodeConfig. +type NodeNetworkConfigSpec struct { + // Revision stores hash of the NodeConfigRevision that was used to create the NodeNetworkConfig object. + Revision string `json:"revision"` + Layer2 []Layer2NetworkConfigurationSpec `json:"layer2"` + Vrf []VRFRouteConfigurationSpec `json:"vrf"` + RoutingTable []RoutingTableSpec `json:"routingTable"` +} + +// NodeNetworkConfigStatus defines the observed state of NodeConfig. +type NodeNetworkConfigStatus struct { + // ConfigStatus describes provisioning state od the NodeConfig. Can be either 'provisioning' or 'provisioned'. + ConfigStatus string `json:"configStatus"` + // LastUpdate determines when last update (change) of the ConfigStatus field took place. + LastUpdate metav1.Time `json:"lastUpdate"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:shortName=nnc,scope=Cluster +//+kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.configStatus` +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// NodeNetworkConfig is the Schema for the node configuration. +type NodeNetworkConfig struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NodeNetworkConfigSpec `json:"spec,omitempty"` + Status NodeNetworkConfigStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// NodeNetworkConfigList contains a list of NodeConfig. +type NodeNetworkConfigList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NodeNetworkConfig `json:"items"` +} + +func NewEmptyConfig(name string) *NodeNetworkConfig { + return &NodeNetworkConfig{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: NodeNetworkConfigSpec{ + Vrf: []VRFRouteConfigurationSpec{}, + Layer2: []Layer2NetworkConfigurationSpec{}, + RoutingTable: []RoutingTableSpec{}, + }, + Status: NodeNetworkConfigStatus{ + ConfigStatus: "", + }, + } +} + +func init() { + SchemeBuilder.Register(&NodeNetworkConfig{}, &NodeNetworkConfigList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ecaa9148..afb46ad8 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -129,6 +129,205 @@ func (in *Layer2NetworkConfigurationStatus) DeepCopy() *Layer2NetworkConfigurati return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkConfigRevision) DeepCopyInto(out *NetworkConfigRevision) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkConfigRevision. +func (in *NetworkConfigRevision) DeepCopy() *NetworkConfigRevision { + if in == nil { + return nil + } + out := new(NetworkConfigRevision) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NetworkConfigRevision) 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 *NetworkConfigRevisionList) DeepCopyInto(out *NetworkConfigRevisionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NetworkConfigRevision, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkConfigRevisionList. +func (in *NetworkConfigRevisionList) DeepCopy() *NetworkConfigRevisionList { + if in == nil { + return nil + } + out := new(NetworkConfigRevisionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NetworkConfigRevisionList) 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 *NetworkConfigRevisionSpec) DeepCopyInto(out *NetworkConfigRevisionSpec) { + *out = *in + in.Config.DeepCopyInto(&out.Config) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkConfigRevisionSpec. +func (in *NetworkConfigRevisionSpec) DeepCopy() *NetworkConfigRevisionSpec { + if in == nil { + return nil + } + out := new(NetworkConfigRevisionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkConfigRevisionStatus) DeepCopyInto(out *NetworkConfigRevisionStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkConfigRevisionStatus. +func (in *NetworkConfigRevisionStatus) DeepCopy() *NetworkConfigRevisionStatus { + if in == nil { + return nil + } + out := new(NetworkConfigRevisionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeNetworkConfig) DeepCopyInto(out *NodeNetworkConfig) { + *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 NodeNetworkConfig. +func (in *NodeNetworkConfig) DeepCopy() *NodeNetworkConfig { + if in == nil { + return nil + } + out := new(NodeNetworkConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodeNetworkConfig) 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 *NodeNetworkConfigList) DeepCopyInto(out *NodeNetworkConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NodeNetworkConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeNetworkConfigList. +func (in *NodeNetworkConfigList) DeepCopy() *NodeNetworkConfigList { + if in == nil { + return nil + } + out := new(NodeNetworkConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NodeNetworkConfigList) 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 *NodeNetworkConfigSpec) DeepCopyInto(out *NodeNetworkConfigSpec) { + *out = *in + if in.Layer2 != nil { + in, out := &in.Layer2, &out.Layer2 + *out = make([]Layer2NetworkConfigurationSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Vrf != nil { + in, out := &in.Vrf, &out.Vrf + *out = make([]VRFRouteConfigurationSpec, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RoutingTable != nil { + in, out := &in.RoutingTable, &out.RoutingTable + *out = make([]RoutingTableSpec, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeNetworkConfigSpec. +func (in *NodeNetworkConfigSpec) DeepCopy() *NodeNetworkConfigSpec { + if in == nil { + return nil + } + out := new(NodeNetworkConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeNetworkConfigStatus) DeepCopyInto(out *NodeNetworkConfigStatus) { + *out = *in + in.LastUpdate.DeepCopyInto(&out.LastUpdate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeNetworkConfigStatus. +func (in *NodeNetworkConfigStatus) DeepCopy() *NodeNetworkConfigStatus { + if in == nil { + return nil + } + out := new(NodeNetworkConfigStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RoutingTable) DeepCopyInto(out *RoutingTable) { *out = *in diff --git a/cmd/manager/main.go b/cmd/agent/main.go similarity index 83% rename from cmd/manager/main.go rename to cmd/agent/main.go index 06d5788e..5d97b69e 100644 --- a/cmd/manager/main.go +++ b/cmd/agent/main.go @@ -34,6 +34,7 @@ import ( "github.com/telekom/das-schiff-network-operator/pkg/anycast" "github.com/telekom/das-schiff-network-operator/pkg/bpf" "github.com/telekom/das-schiff-network-operator/pkg/config" + "github.com/telekom/das-schiff-network-operator/pkg/frr" "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" "github.com/telekom/das-schiff-network-operator/pkg/macvlan" "github.com/telekom/das-schiff-network-operator/pkg/managerconfig" @@ -95,6 +96,7 @@ func main() { var onlyBPFMode bool var configFile string var interfacePrefix string + var nodeNetworkConfigPath string flag.StringVar(&configFile, "config", "", "The controller will load its initial configuration from this file. "+ "Omit this flag to use the default configuration values. "+ @@ -103,6 +105,8 @@ func main() { "Only attach BPF to specified interfaces in config. This will not start any reconciliation. Perfect for masters.") flag.StringVar(&interfacePrefix, "macvlan-interface-prefix", "", "Interface prefix for bridge devices for MACVlan sync") + flag.StringVar(&nodeNetworkConfigPath, "nodenetworkconfig-path", reconciler.DefaultNodeNetworkConfigPath, + "Path to store working node configuration.") opts := zap.Options{ Development: true, } @@ -110,27 +114,14 @@ func main() { flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) - var err error - var options manager.Options - if configFile != "" { - options, err = managerconfig.Load(configFile, scheme) - if err != nil { - setupLog.Error(err, "unable to load the config file") - os.Exit(1) - } - } else { - options = ctrl.Options{Scheme: scheme} - } - if options.MetricsBindAddress != "0" && options.MetricsBindAddress != "" { - err = initCollectors() - if err != nil { - setupLog.Error(err, "unable to initialize metrics collectors") - os.Exit(1) - } + options, err := setManagerOptions(configFile) + if err != nil { + setupLog.Error(err, "unable to configure manager's options") + os.Exit(1) } clientConfig := ctrl.GetConfigOrDie() - mgr, err := ctrl.NewManager(clientConfig, options) + mgr, err := ctrl.NewManager(clientConfig, *options) if err != nil { setupLog.Error(err, "unable to start manager") os.Exit(1) @@ -149,7 +140,7 @@ func main() { os.Exit(1) } - if err := initComponents(mgr, anycastTracker, cfg, clientConfig, onlyBPFMode); err != nil { + if err := initComponents(mgr, anycastTracker, cfg, clientConfig, onlyBPFMode, nodeNetworkConfigPath); err != nil { setupLog.Error(err, "unable to initialize components") os.Exit(1) } @@ -166,7 +157,29 @@ func main() { } } -func initComponents(mgr manager.Manager, anycastTracker *anycast.Tracker, cfg *config.Config, clientConfig *rest.Config, onlyBPFMode bool) error { +func setManagerOptions(configFile string) (*manager.Options, error) { + var err error + var options manager.Options + if configFile != "" { + options, err = managerconfig.Load(configFile, scheme) + if err != nil { + return nil, fmt.Errorf("unable to load the config file: %w", err) + } + } else { + options = ctrl.Options{Scheme: scheme} + } + + if options.MetricsBindAddress != "0" && options.MetricsBindAddress != "" { + err = initCollectors() + if err != nil { + return nil, fmt.Errorf("unable to initialize metrics collectors: %w", err) + } + } + + return &options, nil +} + +func initComponents(mgr manager.Manager, anycastTracker *anycast.Tracker, cfg *config.Config, clientConfig *rest.Config, onlyBPFMode bool, nodeConfigPath string) error { //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { @@ -218,48 +231,36 @@ func initComponents(mgr manager.Manager, anycastTracker *anycast.Tracker, cfg *c return fmt.Errorf("error performing healthcheck: %w", err) } } else { - // Start VRFRouteConfigurationReconciler when we are not running in only BPF mode. - r, err := reconciler.NewReconciler(mgr.GetClient(), anycastTracker, mgr.GetLogger()) + r, err := setupReconcilers(mgr, anycastTracker, nodeConfigPath) if err != nil { - return fmt.Errorf("unable to create debounced reconciler: %w", err) - } - if err := setupReconcilers(r, mgr); err != nil { return fmt.Errorf("unable to setup reconcilers: %w", err) } // Trigger initial reconciliation. - r.Reconcile(context.Background()) + if r != nil { + r.Reconcile(context.Background()) + } } return nil } -func setupReconcilers(r *reconciler.Reconciler, mgr manager.Manager) error { - if err := (&controllers.VRFRouteConfigurationReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Reconciler: r, - }).SetupWithManager(mgr); err != nil { - return fmt.Errorf("unable to create VRFRouteConfiguration controller: %w", err) - } - - if err := (&controllers.Layer2NetworkConfigurationReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Reconciler: r, - }).SetupWithManager(mgr); err != nil { - return fmt.Errorf("unable to create Layer2NetworkConfiguration controller: %w", err) +func setupReconcilers(mgr manager.Manager, anycastTracker *anycast.Tracker, nodeConfigPath string) (*reconciler.NodeNetworkConfigReconciler, error) { + r, err := reconciler.NewNodeNetworkConfigReconciler(mgr.GetClient(), anycastTracker, mgr.GetLogger(), + nodeConfigPath, frr.NewFRRManager(), nl.NewManager(&nl.Toolkit{})) + if err != nil { + return nil, fmt.Errorf("unable to create debounced reconciler: %w", err) } - if err := (&controllers.RoutingTableReconciler{ + if err = (&controllers.NodeNetworkConfigReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), Reconciler: r, }).SetupWithManager(mgr); err != nil { - return fmt.Errorf("unable to create RoutingTable controller: %w", err) + return nil, fmt.Errorf("unable to create NodeConfig controller: %w", err) } - return nil + return r, nil } func performNetworkingHealthcheck(hc *healthcheck.HealthChecker) error { diff --git a/cmd/operator/main.go b/cmd/operator/main.go new file mode 100644 index 00000000..57c791f9 --- /dev/null +++ b/cmd/operator/main.go @@ -0,0 +1,202 @@ +/* +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. +*/ + +//nolint:gci +package main + +import ( + "context" + "flag" + "fmt" + "os" + "time" + + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/controllers" + "github.com/telekom/das-schiff-network-operator/pkg/managerconfig" + "github.com/telekom/das-schiff-network-operator/pkg/reconciler" + "github.com/telekom/das-schiff-network-operator/pkg/version" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) //nolint:gci + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + //nolint:gci // kubebuilder import + //+kubebuilder:scaffold:imports +) + +var ( + scheme = runtime.NewScheme() + setupLog = ctrl.Log.WithName("setup") +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) + + utilruntime.Must(networkv1alpha1.AddToScheme(scheme)) + //+kubebuilder:scaffold:scheme +} + +func main() { + version.Get().Print(os.Args[0]) + + var configFile string + var apiTimeout string + var configTimeout string + var preconfigTimeout string + var maxUpdating int + flag.StringVar(&configFile, "config", "", + "The controller will load its initial configuration from this file. "+ + "Omit this flag to use the default configuration values. "+ + "Command-line flags override configuration from this file.") + flag.StringVar(&apiTimeout, "api-timeout", reconciler.DefaultTimeout, + "Timeout for Kubernetes API connections (default: 60s).") + flag.StringVar(&preconfigTimeout, "preconfig-timeout", reconciler.DefaultPreconfigTimout, "Timoeut for NodeConfig reconciliation process, when agent DID NOT picked the work yet") + flag.StringVar(&configTimeout, "config-timeout", reconciler.DefaultConfigTimeout, "Timoeut for NodeConfig reconciliation process, when agent picked the work") + flag.IntVar(&maxUpdating, "max-updating", 1, "Configures how many nodes can be updated simultaneously when rolling update is performed.") + opts := zap.Options{ + Development: true, + } + opts.BindFlags(flag.CommandLine) + flag.Parse() + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + + options, err := setMangerOptions(configFile) + if err != nil { + setupLog.Error(err, "error configuring manager options") + os.Exit(1) + } + + clientConfig := ctrl.GetConfigOrDie() + mgr, err := ctrl.NewManager(clientConfig, *options) + if err != nil { + setupLog.Error(err, "unable to start manager") + os.Exit(1) + } + + err = setupReconcilers(mgr, apiTimeout, configTimeout, preconfigTimeout, maxUpdating) + if err != nil { + setupLog.Error(err, "unable to setup reconcilers") + os.Exit(1) + } + + setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + setupLog.Error(err, "problem running manager") + os.Exit(1) + } +} + +func setupReconcilers(mgr manager.Manager, apiTimeout, configTimeout, preconfigTimeout string, maxUpdating int) error { + apiTimoutVal, err := time.ParseDuration(apiTimeout) + if err != nil { + return fmt.Errorf("error parsing API timeout value %s: %w", apiTimeout, err) + } + + configTimeoutVal, err := time.ParseDuration(configTimeout) + if err != nil { + return fmt.Errorf("error parsing config timeout value %s: %w", configTimeout, err) + } + + preconfigTimeoutVal, err := time.ParseDuration(preconfigTimeout) + if err != nil { + return fmt.Errorf("error parsing preconfig timeout value %s: %w", preconfigTimeout, err) + } + + cr, err := reconciler.NewConfigReconciler(mgr.GetClient(), mgr.GetLogger().WithName("ConfigReconciler"), apiTimoutVal) + if err != nil { + return fmt.Errorf("unable to create config reconciler reconciler: %w", err) + } + + ncr, err := reconciler.NewNodeConfigReconciler(mgr.GetClient(), mgr.GetLogger().WithName("NodeConfigReconciler"), apiTimoutVal, configTimeoutVal, preconfigTimeoutVal, mgr.GetScheme(), maxUpdating) + if err != nil { + return fmt.Errorf("unable to create node reconciler: %w", err) + } + + initialSetup := newOnLeaderElectionEvent(cr) + if err := mgr.Add(initialSetup); err != nil { + return fmt.Errorf("error adding on leader election event to the manager: %w", err) + } + + if err = (&controllers.ConfigReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Reconciler: cr, + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create Config controller: %w", err) + } + + if err = (&controllers.RevisionReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Reconciler: ncr, + }).SetupWithManager(mgr); err != nil { + return fmt.Errorf("unable to create RoutingTable controller: %w", err) + } + + return nil +} + +func setMangerOptions(configFile string) (*manager.Options, error) { + var err error + var options manager.Options + if configFile != "" { + options, err = managerconfig.Load(configFile, scheme) + if err != nil { + return nil, fmt.Errorf("unable to load the config file: %w", err) + } + } else { + options = ctrl.Options{Scheme: scheme} + } + + // force leader election + options.LeaderElection = true + if options.LeaderElectionID == "" { + options.LeaderElectionID = "network-operator" + } + + // force turn off metrics server + options.MetricsBindAddress = "0" + + return &options, nil +} + +type onLeaderElectionEvent struct { + cr *reconciler.ConfigReconciler +} + +func newOnLeaderElectionEvent(cr *reconciler.ConfigReconciler) *onLeaderElectionEvent { + return &onLeaderElectionEvent{ + cr: cr, + } +} + +func (*onLeaderElectionEvent) NeedLeaderElection() bool { + return true +} + +func (e *onLeaderElectionEvent) Start(ctx context.Context) error { + if err := e.cr.ReconcileDebounced(ctx); err != nil { + return fmt.Errorf("error configuring initial configuration revision: %w", err) + } + return nil +} diff --git a/config/manager/manager.yaml b/config/agent/agent.yaml similarity index 92% rename from config/manager/manager.yaml rename to config/agent/agent.yaml index 16497def..a33a2add 100644 --- a/config/manager/manager.yaml +++ b/config/agent/agent.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: DaemonSet metadata: - name: worker + name: agent namespace: system labels: - app.kubernetes.io/component: worker + app.kubernetes.io/component: agent spec: selector: matchLabels: - app.kubernetes.io/component: worker + app.kubernetes.io/component: agent template: metadata: annotations: - kubectl.kubernetes.io/default-container: manager + kubectl.kubernetes.io/default-container: agent labels: - app.kubernetes.io/component: worker + app.kubernetes.io/component: agent spec: affinity: nodeAffinity: @@ -37,15 +37,15 @@ spec: hostPID: true containers: - command: - - /manager + - /agent args: [] env: - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - image: controller:latest - name: manager + image: agent:latest + name: agent securityContext: privileged: true runAsUser: 0 diff --git a/config/manager/manager_master.yaml b/config/agent/agent_master.yaml similarity index 96% rename from config/manager/manager_master.yaml rename to config/agent/agent_master.yaml index cd3a4917..65ea2e1d 100644 --- a/config/manager/manager_master.yaml +++ b/config/agent/agent_master.yaml @@ -12,7 +12,7 @@ spec: template: metadata: annotations: - kubectl.kubernetes.io/default-container: manager + kubectl.kubernetes.io/default-container: agent labels: app.kubernetes.io/component: master spec: @@ -40,7 +40,7 @@ spec: hostPID: true containers: - command: - - /manager + - /agent args: - -only-attach-bpf env: @@ -48,8 +48,8 @@ spec: valueFrom: fieldRef: fieldPath: spec.nodeName - image: controller:latest - name: manager + image: agent:latest + name: agent securityContext: privileged: true runAsUser: 0 diff --git a/config/manager/config.yaml b/config/agent/config.yaml similarity index 100% rename from config/manager/config.yaml rename to config/agent/config.yaml diff --git a/config/manager/controller_manager_config.yaml b/config/agent/controller_agent_config.yaml similarity index 100% rename from config/manager/controller_manager_config.yaml rename to config/agent/controller_agent_config.yaml diff --git a/config/manager/kustomization.yaml b/config/agent/kustomization.yaml similarity index 66% rename from config/manager/kustomization.yaml rename to config/agent/kustomization.yaml index e30e1a4f..3e53816d 100644 --- a/config/manager/kustomization.yaml +++ b/config/agent/kustomization.yaml @@ -1,6 +1,6 @@ resources: -- manager.yaml -- manager_master.yaml +- agent.yaml +- agent_master.yaml - service.yaml # - namespace.yaml @@ -9,16 +9,16 @@ generatorOptions: configMapGenerator: - files: - - controller_manager_config.yaml - name: manager-config + - controller_agent_config.yaml + name: agent-config - files: - config.yaml name: config apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: -- name: controller - newName: ghcr.io/telekom/das-schiff-network-operator +- name: agent + newName: ghcr.io/telekom/das-schiff-network-operator-agent newTag: latest - name: frr-exporter newName: ghcr.io/telekom/frr-exporter diff --git a/config/manager/namespace.yaml b/config/agent/namespace.yaml similarity index 100% rename from config/manager/namespace.yaml rename to config/agent/namespace.yaml diff --git a/config/manager/service.yaml b/config/agent/service.yaml similarity index 100% rename from config/manager/service.yaml rename to config/agent/service.yaml diff --git a/config/certmanager/kustomization.yaml b/config/certmanager/kustomization.yaml index bebea5a5..ff414e3c 100644 --- a/config/certmanager/kustomization.yaml +++ b/config/certmanager/kustomization.yaml @@ -1,3 +1,6 @@ +# Adds namespace to all resources. +namespace: kube-system + resources: - certificate.yaml diff --git a/config/crd/bases/network.schiff.telekom.de_networkconfigrevisions.yaml b/config/crd/bases/network.schiff.telekom.de_networkconfigrevisions.yaml new file mode 100644 index 00000000..833203b0 --- /dev/null +++ b/config/crd/bases/network.schiff.telekom.de_networkconfigrevisions.yaml @@ -0,0 +1,320 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: networkconfigrevisions.network.schiff.telekom.de +spec: + group: network.schiff.telekom.de + names: + kind: NetworkConfigRevision + listKind: NetworkConfigRevisionList + plural: networkconfigrevisions + shortNames: + - ncr + singular: networkconfigrevision + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.isInvalid + name: Invalid + type: string + - jsonPath: .status.queued + name: Queued + type: integer + - jsonPath: .status.ongoing + name: Ongoing + type: integer + - jsonPath: .status.ready + name: Ready + type: integer + - jsonPath: .status.total + name: Total + type: integer + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: NetworkConfigRevision is the Schema for the node configuration. + 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: NetworkConfigSpec defines the desired state of NetworkConfig. + properties: + config: + description: Config stores global configuration of the nodes. + properties: + layer2: + items: + description: Layer2NetworkConfigurationSpec defines the desired + state of Layer2NetworkConfiguration. + properties: + advertiseNeighbors: + description: If desired network-operator advertises host + routes for local neighbors + type: boolean + anycastGateways: + description: Anycast Gateway to configure on bridge + items: + type: string + type: array + anycastMac: + description: If anycast is desired, specify anycast gateway + MAC address + pattern: (?:[[:xdigit:]]{2}:){5}[[:xdigit:]]{2} + type: string + createMacVLANInterface: + description: Create MACVLAN attach interface + type: boolean + id: + description: VLAN Id of the layer 2 network + type: integer + mtu: + description: Network interface MTU + maximum: 9000 + minimum: 1000 + type: integer + neighSuppression: + description: Enable ARP / ND suppression + type: boolean + nodeSelector: + description: Select nodes to create Layer2 network on + 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 + required: + - key + - operator + type: object + type: array + 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 + vni: + description: VXLAN VNI Id for the layer 2 network + maximum: 16777215 + minimum: 1 + type: integer + vrf: + description: VRF to attach Layer2 network to, default if + not set + type: string + required: + - id + - mtu + - vni + type: object + type: array + revision: + description: Revision stores hash of the NodeConfigRevision that + was used to create the NodeNetworkConfig object. + type: string + routingTable: + items: + description: RoutingTableSpec defines the desired state of RoutingTable. + properties: + tableId: + description: TableID is the host table that can be used + to export routes + type: integer + required: + - tableId + type: object + type: array + vrf: + items: + description: VRFRouteConfigurationSpec defines the desired state + of VRFRouteConfiguration. + properties: + aggregate: + description: Aggregate Routes that should be announced + items: + type: string + type: array + community: + description: Community for export, if omitted no community + will be set + type: string + export: + description: Routes exported from the cluster VRF into the + specified VRF + items: + description: VRFRouteConfigurationPrefixItem defines a + prefix item. + properties: + action: + enum: + - permit + - deny + type: string + cidr: + description: CIDR of the leaked network + type: string + ge: + description: Minimum prefix length to be matched + type: integer + le: + description: Maximum prefix length to be matched + type: integer + seq: + description: Sequence in the generated prefix-list, + if omitted will be list index + maximum: 4294967295 + minimum: 1 + type: integer + required: + - action + type: object + maxItems: 4294967295 + type: array + import: + description: Routes imported from this VRF into the cluster + VRF + items: + description: VRFRouteConfigurationPrefixItem defines a + prefix item. + properties: + action: + enum: + - permit + - deny + type: string + cidr: + description: CIDR of the leaked network + type: string + ge: + description: Minimum prefix length to be matched + type: integer + le: + description: Maximum prefix length to be matched + type: integer + seq: + description: Sequence in the generated prefix-list, + if omitted will be list index + maximum: 4294967295 + minimum: 1 + type: integer + required: + - action + type: object + maxItems: 4294967295 + type: array + mtu: + default: 9000 + description: The MTU of the VRF + type: integer + seq: + description: Sequence of the generated route-map, maximum + of 65534 because we sometimes have to set an explicit + default-deny + maximum: 65534 + minimum: 1 + type: integer + vrf: + description: VRF this configuration refers to + maxLength: 12 + type: string + required: + - export + - import + - seq + type: object + type: array + required: + - layer2 + - revision + - routingTable + - vrf + type: object + revision: + description: Revision is a hash of the NetworkConfigRevision object + that is used to identify the particular revision. + type: string + required: + - config + - revision + type: object + status: + properties: + isInvalid: + description: IsInvalid determines if NetworkConfigRevision results + in misconfigured nodes (invalid configuration). + type: boolean + ongoing: + description: Ongoing informs about how many nodes are currently provisioned + with a config derived from the revision. + type: integer + queued: + description: Queued informs about how many nodes are currently waiting + to be provisiined with a config derived from the revision. + type: integer + ready: + description: Ready informs about how many nodes were already provisioned + with a config derived from the revision. + type: integer + total: + description: Total informs about how many nodes in total can be provisiined + with a config derived from the revision. + type: integer + required: + - isInvalid + - ongoing + - queued + - ready + - total + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/network.schiff.telekom.de_nodenetworkconfigs.yaml b/config/crd/bases/network.schiff.telekom.de_nodenetworkconfigs.yaml new file mode 100644 index 00000000..4421b173 --- /dev/null +++ b/config/crd/bases/network.schiff.telekom.de_nodenetworkconfigs.yaml @@ -0,0 +1,283 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: nodenetworkconfigs.network.schiff.telekom.de +spec: + group: network.schiff.telekom.de + names: + kind: NodeNetworkConfig + listKind: NodeNetworkConfigList + plural: nodenetworkconfigs + shortNames: + - nnc + singular: nodenetworkconfig + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.configStatus + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: NodeNetworkConfig is the Schema for the node configuration. + 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: NodeNetworkConfigSpec defines the desired state of NodeConfig. + properties: + layer2: + items: + description: Layer2NetworkConfigurationSpec defines the desired + state of Layer2NetworkConfiguration. + properties: + advertiseNeighbors: + description: If desired network-operator advertises host routes + for local neighbors + type: boolean + anycastGateways: + description: Anycast Gateway to configure on bridge + items: + type: string + type: array + anycastMac: + description: If anycast is desired, specify anycast gateway + MAC address + pattern: (?:[[:xdigit:]]{2}:){5}[[:xdigit:]]{2} + type: string + createMacVLANInterface: + description: Create MACVLAN attach interface + type: boolean + id: + description: VLAN Id of the layer 2 network + type: integer + mtu: + description: Network interface MTU + maximum: 9000 + minimum: 1000 + type: integer + neighSuppression: + description: Enable ARP / ND suppression + type: boolean + nodeSelector: + description: Select nodes to create Layer2 network on + 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 + required: + - key + - operator + type: object + type: array + 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 + vni: + description: VXLAN VNI Id for the layer 2 network + maximum: 16777215 + minimum: 1 + type: integer + vrf: + description: VRF to attach Layer2 network to, default if not + set + type: string + required: + - id + - mtu + - vni + type: object + type: array + revision: + description: Revision stores hash of the NodeConfigRevision that was + used to create the NodeNetworkConfig object. + type: string + routingTable: + items: + description: RoutingTableSpec defines the desired state of RoutingTable. + properties: + tableId: + description: TableID is the host table that can be used to export + routes + type: integer + required: + - tableId + type: object + type: array + vrf: + items: + description: VRFRouteConfigurationSpec defines the desired state + of VRFRouteConfiguration. + properties: + aggregate: + description: Aggregate Routes that should be announced + items: + type: string + type: array + community: + description: Community for export, if omitted no community will + be set + type: string + export: + description: Routes exported from the cluster VRF into the specified + VRF + items: + description: VRFRouteConfigurationPrefixItem defines a prefix + item. + properties: + action: + enum: + - permit + - deny + type: string + cidr: + description: CIDR of the leaked network + type: string + ge: + description: Minimum prefix length to be matched + type: integer + le: + description: Maximum prefix length to be matched + type: integer + seq: + description: Sequence in the generated prefix-list, if + omitted will be list index + maximum: 4294967295 + minimum: 1 + type: integer + required: + - action + type: object + maxItems: 4294967295 + type: array + import: + description: Routes imported from this VRF into the cluster + VRF + items: + description: VRFRouteConfigurationPrefixItem defines a prefix + item. + properties: + action: + enum: + - permit + - deny + type: string + cidr: + description: CIDR of the leaked network + type: string + ge: + description: Minimum prefix length to be matched + type: integer + le: + description: Maximum prefix length to be matched + type: integer + seq: + description: Sequence in the generated prefix-list, if + omitted will be list index + maximum: 4294967295 + minimum: 1 + type: integer + required: + - action + type: object + maxItems: 4294967295 + type: array + mtu: + default: 9000 + description: The MTU of the VRF + type: integer + seq: + description: Sequence of the generated route-map, maximum of + 65534 because we sometimes have to set an explicit default-deny + maximum: 65534 + minimum: 1 + type: integer + vrf: + description: VRF this configuration refers to + maxLength: 12 + type: string + required: + - export + - import + - seq + type: object + type: array + required: + - layer2 + - revision + - routingTable + - vrf + type: object + status: + description: NodeNetworkConfigStatus defines the observed state of NodeConfig. + properties: + configStatus: + description: ConfigStatus describes provisioning state od the NodeConfig. + Can be either 'provisioning' or 'provisioned'. + type: string + lastUpdate: + description: LastUpdate determines when last update (change) of the + ConfigStatus field took place. + format: date-time + type: string + required: + - configStatus + - lastUpdate + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 2ecd84e3..1b371db8 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -5,6 +5,8 @@ resources: - bases/network.schiff.telekom.de_vrfrouteconfigurations.yaml - bases/network.schiff.telekom.de_layer2networkconfigurations.yaml - bases/network.schiff.telekom.de_routingtables.yaml +- bases/network.schiff.telekom.de_nodenetworkconfigs.yaml +- bases/network.schiff.telekom.de_networkconfigrevisions.yaml #+kubebuilder:scaffold:crdkustomizeresource # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 72d9c77d..9e6bca37 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -42,7 +42,8 @@ kind: Kustomization resources: - ../crd - ../rbac -- ../manager +- ../agent +- ../operator - ../webhook - ../prometheus labels: diff --git a/config/default/manager_auth_proxy_patch.yaml b/config/default/manager_auth_proxy_patch.yaml index 7a48a5ff..e05af3fa 100644 --- a/config/default/manager_auth_proxy_patch.yaml +++ b/config/default/manager_auth_proxy_patch.yaml @@ -3,7 +3,7 @@ apiVersion: apps/v1 kind: DaemonSet metadata: - name: worker + name: agent namespace: system spec: template: diff --git a/config/default/manager_config_patch.yaml b/config/default/manager_config_patch.yaml index 0defb7cd..43b69476 100644 --- a/config/default/manager_config_patch.yaml +++ b/config/default/manager_config_patch.yaml @@ -1,20 +1,20 @@ apiVersion: apps/v1 kind: DaemonSet metadata: - name: worker + name: agent namespace: system spec: template: spec: containers: - - name: manager + - name: agent args: - - "--config=controller_manager_config.yaml" + - "--config=controller_agent_config.yaml" volumeMounts: - - name: manager-config - mountPath: /controller_manager_config.yaml - subPath: controller_manager_config.yaml + - name: agent-config + mountPath: /controller_agent_config.yaml + subPath: controller_agent_config.yaml volumes: - - name: manager-config + - name: agent-config configMap: - name: manager-config + name: agent-config diff --git a/config/default/manager_master_config_patch.yaml b/config/default/manager_master_config_patch.yaml index 82614073..d8a9b654 100644 --- a/config/default/manager_master_config_patch.yaml +++ b/config/default/manager_master_config_patch.yaml @@ -7,14 +7,14 @@ spec: template: spec: containers: - - name: manager + - name: agent args: - - "--config=controller_manager_config.yaml" + - "--config=controller_agent_config.yaml" volumeMounts: - - name: manager-config - mountPath: /controller_manager_config.yaml - subPath: controller_manager_config.yaml + - name: agent-config + mountPath: /controller_agent_config.yaml + subPath: controller_agent_config.yaml volumes: - - name: manager-config + - name: agent-config configMap: - name: manager-config + name: agent-config diff --git a/config/default/manager_master_metrics_patch.yaml b/config/default/manager_master_metrics_patch.yaml index 3d732a5b..8116aa7a 100644 --- a/config/default/manager_master_metrics_patch.yaml +++ b/config/default/manager_master_metrics_patch.yaml @@ -7,7 +7,7 @@ spec: template: spec: containers: - - name: manager + - name: agent ports: - containerPort: 7080 name: metrics diff --git a/config/default/manager_master_webhook_patch.yaml b/config/default/manager_master_webhook_patch.yaml index 5699ef52..49e1f0f2 100644 --- a/config/default/manager_master_webhook_patch.yaml +++ b/config/default/manager_master_webhook_patch.yaml @@ -7,7 +7,7 @@ spec: template: spec: containers: - - name: manager + - name: agent ports: - containerPort: 7443 name: webhook-server diff --git a/config/default/manager_metrics_patch.yaml b/config/default/manager_metrics_patch.yaml index 93366657..aac2b23c 100644 --- a/config/default/manager_metrics_patch.yaml +++ b/config/default/manager_metrics_patch.yaml @@ -1,13 +1,13 @@ apiVersion: apps/v1 kind: DaemonSet metadata: - name: worker + name: agent namespace: system spec: template: spec: containers: - - name: manager + - name: agent ports: - containerPort: 7080 name: metrics diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml index eebe8977..3f98ef6d 100644 --- a/config/default/manager_webhook_patch.yaml +++ b/config/default/manager_webhook_patch.yaml @@ -1,13 +1,13 @@ apiVersion: apps/v1 kind: DaemonSet metadata: - name: worker + name: agent namespace: system spec: template: spec: containers: - - name: manager + - name: agent ports: - containerPort: 7443 name: webhook-server diff --git a/config/operator/kustomization.yaml b/config/operator/kustomization.yaml new file mode 100644 index 00000000..59de5691 --- /dev/null +++ b/config/operator/kustomization.yaml @@ -0,0 +1,12 @@ +resources: +- operator.yaml + +generatorOptions: + disableNameSuffixHash: true + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: operator + newName: ghcr.io/telekom/das-schiff-network-opeator + newTag: latest diff --git a/config/operator/operator.yaml b/config/operator/operator.yaml new file mode 100644 index 00000000..d8b8da56 --- /dev/null +++ b/config/operator/operator.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: operator + namespace: system + labels: + app.kubernetes.io/component: operator +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/component: operator + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: operator + labels: + app.kubernetes.io/component: operator + spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role.kubernetes.io/control-plane + operator: DoesNotExist + tolerations: + - effect: NoSchedule + key: node.schiff.telekom.de/uninitialized + operator: Exists + - key: node.cloudprovider.kubernetes.io/uninitialized + value: "true" + effect: NoSchedule + - key: node.kubernetes.io/not-ready + effect: NoSchedule + operator: Exists + hostNetwork: true + hostPID: true + containers: + - command: + - /operator + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + image: operator:latest + name: operator + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + - mountPath: /var/state + name: state + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 + volumes: + - name: state + hostPath: + path: /var/state + type: DirectoryOrCreate diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index b58d5a05..de3c9fa8 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -51,6 +51,58 @@ rules: - get - patch - update +- apiGroups: + - network.schiff.telekom.de + resources: + - networkconfigrevisions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - network.schiff.telekom.de + resources: + - networkconfigrevisions/finalizers + verbs: + - update +- apiGroups: + - network.schiff.telekom.de + resources: + - networkconfigrevisions/status + verbs: + - get + - patch + - update +- apiGroups: + - network.schiff.telekom.de + resources: + - nodenetworkconfigs + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - network.schiff.telekom.de + resources: + - nodenetworkconfigs/finalizers + verbs: + - update +- apiGroups: + - network.schiff.telekom.de + resources: + - nodenetworkconfigs/status + verbs: + - get + - patch + - update - apiGroups: - network.schiff.telekom.de resources: diff --git a/controllers/layer2networkconfiguration_controller.go b/controllers/config_controller.go similarity index 58% rename from controllers/layer2networkconfiguration_controller.go rename to controllers/config_controller.go index 54396c27..c8e32410 100644 --- a/controllers/layer2networkconfiguration_controller.go +++ b/controllers/config_controller.go @@ -19,46 +19,46 @@ package controllers import ( "context" "fmt" - "os" "time" - "github.com/google/go-cmp/cmp" networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" - "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" "github.com/telekom/das-schiff-network-operator/pkg/reconciler" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" 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/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" ) const requeueTime = 10 * time.Minute -// Layer2NetworkConfigurationReconciler reconciles a Layer2NetworkConfiguration object. -type Layer2NetworkConfigurationReconciler struct { +// ConfigReconciler reconciles a Layer2NetworkConfiguration, RoutingTable and VRFRouteConfiguration objects. +type ConfigReconciler struct { client.Client Scheme *runtime.Scheme - Reconciler *reconciler.Reconciler + Reconciler *reconciler.ConfigReconciler } -//+kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;update;watch //+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=layer2networkconfigurations,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=layer2networkconfigurations/status,verbs=get;update;patch //+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=layer2networkconfigurations/finalizers,verbs=update +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=routingtables,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=routingtables/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=routingtables/finalizers,verbs=update + +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=vrfrouteconfigurations,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=vrfrouteconfigurations/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=vrfrouteconfigurations/finalizers,verbs=update + // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile -func (r *Layer2NetworkConfigurationReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { +func (r *ConfigReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) r.Reconciler.Reconcile(ctx) @@ -67,21 +67,13 @@ func (r *Layer2NetworkConfigurationReconciler) Reconcile(ctx context.Context, _ } // SetupWithManager sets up the controller with the Manager. -func (r *Layer2NetworkConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { - // Create empty request for changes to node - nodesMapFn := handler.EnqueueRequestsFromMapFunc(func(_ context.Context, _ client.Object) []reconcile.Request { return []reconcile.Request{{}} }) - nodePredicates := predicate.Funcs{ - CreateFunc: func(_ event.CreateEvent) bool { return false }, - UpdateFunc: func(e event.UpdateEvent) bool { - return os.Getenv(healthcheck.NodenameEnv) == e.ObjectNew.GetName() && !cmp.Equal(e.ObjectNew.GetLabels(), e.ObjectOld.GetLabels()) - }, - DeleteFunc: func(_ event.DeleteEvent) bool { return false }, - GenericFunc: func(_ event.GenericEvent) bool { return false }, - } - +func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + h := handler.EnqueueRequestsFromMapFunc(func(_ context.Context, _ client.Object) []reconcile.Request { return []ctrl.Request{{}} }) err := ctrl.NewControllerManagedBy(mgr). - For(&networkv1alpha1.Layer2NetworkConfiguration{}). - Watches(&corev1.Node{}, nodesMapFn, builder.WithPredicates(nodePredicates)). + Named("config controller"). + Watches(&networkv1alpha1.Layer2NetworkConfiguration{}, h). + Watches(&networkv1alpha1.RoutingTable{}, h). + Watches(&networkv1alpha1.VRFRouteConfiguration{}, h). Complete(r) if err != nil { return fmt.Errorf("error creating controller: %w", err) diff --git a/controllers/nodenetworkconfig_controller.go b/controllers/nodenetworkconfig_controller.go new file mode 100644 index 00000000..86ec65a3 --- /dev/null +++ b/controllers/nodenetworkconfig_controller.go @@ -0,0 +1,85 @@ +/* +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 controllers + +import ( + "context" + "fmt" + "os" + "strings" + + networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" + "github.com/telekom/das-schiff-network-operator/pkg/reconciler" + "k8s.io/apimachinery/pkg/runtime" + 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/event" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// NodeNetworkConfigReconciler reconciles a NodeNetworkConfig object. +type NodeNetworkConfigReconciler struct { + client.Client + Scheme *runtime.Scheme + + Reconciler *reconciler.NodeNetworkConfigReconciler +} + +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=nodenetworkconfigs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=nodenetworkconfigs/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=nodenetworkconfigs/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile +func (r *NodeNetworkConfigReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + _ = log.FromContext(ctx) + + // Run ReconcileDebounced through debouncer + if err := r.Reconciler.Reconcile(ctx); err != nil { + return ctrl.Result{}, fmt.Errorf("reconicliation error: %w", err) + } + + return ctrl.Result{RequeueAfter: requeueTime}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *NodeNetworkConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { + namePredicates := predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return strings.Contains(e.Object.GetName(), os.Getenv(healthcheck.NodenameEnv)) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return strings.Contains(e.ObjectNew.GetName(), os.Getenv(healthcheck.NodenameEnv)) + }, + DeleteFunc: func(event.DeleteEvent) bool { return false }, + GenericFunc: func(event.GenericEvent) bool { return false }, + } + + err := ctrl.NewControllerManagedBy(mgr). + For(&networkv1alpha1.NodeNetworkConfig{}, builder.WithPredicates(namePredicates)). + Complete(r) + if err != nil { + return fmt.Errorf("error creating controller: %w", err) + } + return nil +} diff --git a/controllers/routingtable_controller.go b/controllers/revision_controller.go similarity index 55% rename from controllers/routingtable_controller.go rename to controllers/revision_controller.go index 47cf8409..5c12403f 100644 --- a/controllers/routingtable_controller.go +++ b/controllers/revision_controller.go @@ -1,5 +1,5 @@ /* -Copyright 2022. +Copyright 2024. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,45 +19,57 @@ package controllers import ( "context" "fmt" + "time" networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" "github.com/telekom/das-schiff-network-operator/pkg/reconciler" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" 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/handler" "sigs.k8s.io/controller-runtime/pkg/log" ) -// RoutingTableReconciler reconciles a RoutingTable object. -type RoutingTableReconciler struct { +const ( + revisionRequeueTime = 1 * time.Minute +) + +// NetworkConfigRevisionReconciler reconciles a NetworkConfigRevision object. +type RevisionReconciler struct { client.Client Scheme *runtime.Scheme - Reconciler *reconciler.Reconciler + Reconciler *reconciler.ConfigRevisionReconciler } -//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=routingtables,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=routingtables/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=routingtables/finalizers,verbs=update +//+kubebuilder:rbac:groups=core,resources=nodes,verbs=get;list;update;watch + +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=networkconfigrevisions,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=networkconfigrevisions/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=networkconfigrevisions/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.4/pkg/reconcile -func (r *RoutingTableReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { +func (r *RevisionReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { _ = log.FromContext(ctx) // Run ReconcileDebounced through debouncer r.Reconciler.Reconcile(ctx) - return ctrl.Result{RequeueAfter: requeueTime}, nil + return ctrl.Result{RequeueAfter: revisionRequeueTime}, nil } // SetupWithManager sets up the controller with the Manager. -func (r *RoutingTableReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *RevisionReconciler) SetupWithManager(mgr ctrl.Manager) error { err := ctrl.NewControllerManagedBy(mgr). - For(&networkv1alpha1.RoutingTable{}). + For(&networkv1alpha1.NetworkConfigRevision{}). + Watches(&corev1.Node{}, &handler.EnqueueRequestForObject{}). + Owns(&networkv1alpha1.NodeNetworkConfig{}, builder.MatchEveryOwner). Complete(r) if err != nil { return fmt.Errorf("error creating controller: %w", err) diff --git a/controllers/vrfrouteconfiguration_controller.go b/controllers/vrfrouteconfiguration_controller.go deleted file mode 100644 index f3d6f153..00000000 --- a/controllers/vrfrouteconfiguration_controller.go +++ /dev/null @@ -1,63 +0,0 @@ -/* -Copyright 2022. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controllers - -import ( - "context" - "fmt" - - networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" - "github.com/telekom/das-schiff-network-operator/pkg/reconciler" - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// VRFRouteConfigurationReconciler reconciles a VRFRouteConfiguration object. -type VRFRouteConfigurationReconciler struct { - client.Client - Scheme *runtime.Scheme - - Reconciler *reconciler.Reconciler -} - -//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=vrfrouteconfigurations,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=vrfrouteconfigurations/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=network.schiff.telekom.de,resources=vrfrouteconfigurations/finalizers,verbs=update - -// Reconcile is part of the main kubernetes reconciliation loop which aims to -// move the current state of the cluster closer to the desired state. -// -// For more details, check Reconcile and its Result here: -// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.10.0/pkg/reconcile -func (r *VRFRouteConfigurationReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { - // Run ReconcileDebounced through debouncer - r.Reconciler.Reconcile(ctx) - - return ctrl.Result{RequeueAfter: requeueTime}, nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *VRFRouteConfigurationReconciler) SetupWithManager(mgr ctrl.Manager) error { - err := ctrl.NewControllerManagedBy(mgr). - For(&networkv1alpha1.VRFRouteConfiguration{}). - Complete(r) - if err != nil { - return fmt.Errorf("error creating controller: %w", err) - } - return nil -} diff --git a/frr-exporter.Dockerfile b/frr-exporter.Dockerfile index de4a836d..4dc26d31 100644 --- a/frr-exporter.Dockerfile +++ b/frr-exporter.Dockerfile @@ -1,7 +1,7 @@ ARG FRR_VERSION="10.1.0" ARG REGISTRY="quay.io" # Build the manager binary -FROM docker.io/library/golang:1.21-alpine as builder +FROM docker.io/library/golang:1.21-alpine AS builder WORKDIR /workspace # Copy the Go Modules manifests diff --git a/go.mod b/go.mod index 2ac3c3f2..52be798b 100644 --- a/go.mod +++ b/go.mod @@ -5,42 +5,43 @@ go 1.21 require ( github.com/cilium/ebpf v0.9.1 github.com/coreos/go-iptables v0.6.0 - github.com/coreos/go-systemd/v22 v22.4.0 - github.com/go-logr/logr v1.2.4 - github.com/google/go-cmp v0.5.9 - github.com/onsi/ginkgo v1.16.4 - github.com/onsi/gomega v1.27.10 - github.com/prometheus/client_golang v1.15.1 + github.com/coreos/go-systemd/v22 v22.5.0 + github.com/go-logr/logr v1.3.0 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.30.0 + github.com/prometheus/client_golang v1.16.0 github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 go.uber.org/mock v0.2.0 golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb - golang.org/x/sys v0.11.0 + golang.org/x/sys v0.15.0 gopkg.in/yaml.v2 v2.4.0 - k8s.io/api v0.27.2 - k8s.io/apimachinery v0.27.2 - k8s.io/client-go v0.27.2 - k8s.io/utils v0.0.0-20230209194617-a36077c30491 + k8s.io/api v0.29.0 + k8s.io/apimachinery v0.29.0 + k8s.io/client-go v0.29.0 + k8s.io/utils v0.0.0-20230726121419-3b25d923346b sigs.k8s.io/controller-runtime v0.15.1 ) +require github.com/google/go-cmp v0.6.0 // indirect + require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect - github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/gnostic v0.5.7-v3refs // indirect - github.com/google/gofuzz v1.1.0 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -53,29 +54,30 @@ require ( github.com/nxadm/tail v1.4.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.42.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect + github.com/prometheus/common v0.44.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect - go.uber.org/atomic v1.7.0 // indirect - go.uber.org/multierr v1.6.0 // indirect + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.24.0 // indirect - golang.org/x/net v0.12.0 // indirect - golang.org/x/oauth2 v0.5.0 // indirect - golang.org/x/term v0.10.0 // indirect - golang.org/x/text v0.11.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.10.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.3.0 // indirect + golang.org/x/tools v0.16.1 // indirect gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.27.2 // indirect - k8s.io/component-base v0.27.2 // indirect - k8s.io/klog/v2 v2.90.1 // indirect - k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect + k8s.io/apiextensions-apiserver v0.29.0 // indirect + k8s.io/component-base v0.29.0 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect - sigs.k8s.io/yaml v1.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 1e12b45e..8c3a18cf 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,21 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cilium/ebpf v0.9.1 h1:64sn2K3UKw8NbP/blsixRpF3nXuyhz/VjRlRzvlBRu4= github.com/cilium/ebpf v0.9.1/go.mod h1:+OhNOIXx/Fnu1IE8bJz2dzOA+VSfyTfdNUVdlQnxUFY= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo4jk= github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-systemd/v22 v22.4.0 h1:y9YHcjnjynCd/DVbg5j9L/33jQM3MxJlbj/zWskzfGU= -github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= -github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= @@ -31,17 +24,17 @@ github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzP github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= -github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= -github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= @@ -51,36 +44,31 @@ github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= -github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= @@ -96,7 +84,6 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -120,33 +107,31 @@ github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= -github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= -github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= -github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= +github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -155,8 +140,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5 h1:b/k/BVWzWRS5v6AB0gf2ckFSbFsHN5jR0HoNso1pN+w= github.com/vishvananda/netlink v1.1.1-0.20211129163951-9ada19101fc5/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae h1:4hwBBUfQCFe3Cym0ZtKyq7L16eZUtYKs+BaHDN6mAns= @@ -164,34 +150,29 @@ github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.2.0 h1:TaP3xedm7JaAgScZO7tlvlKrqT0p7I6OsdGB5YNSMDU= go.uber.org/mock v0.2.0/go.mod h1:J0y0rp9L3xiff1+ZBfKxlC1fz2+aO16tw0tsDOixfuM= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -200,19 +181,16 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= -golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8= +golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -227,64 +205,47 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= -golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= -golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= -golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.3.0 h1:8NFhfS6gzxNqjLIYnZxg319wZ5Qjnx4m/CcX+Klzazc= gomodules.xyz/jsonpatch/v2 v2.3.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -299,32 +260,29 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.27.2 h1:+H17AJpUMvl+clT+BPnKf0E3ksMAzoBBg7CntpSuADo= -k8s.io/api v0.27.2/go.mod h1:ENmbocXfBT2ADujUXcBhHV55RIT31IIEvkntP6vZKS4= -k8s.io/apiextensions-apiserver v0.27.2 h1:iwhyoeS4xj9Y7v8YExhUwbVuBhMr3Q4bd/laClBV6Bo= -k8s.io/apiextensions-apiserver v0.27.2/go.mod h1:Oz9UdvGguL3ULgRdY9QMUzL2RZImotgxvGjdWRq6ZXQ= -k8s.io/apimachinery v0.27.2 h1:vBjGaKKieaIreI+oQwELalVG4d8f3YAMNpWLzDXkxeg= -k8s.io/apimachinery v0.27.2/go.mod h1:XNfZ6xklnMCOGGFNqXG7bUrQCoR04dh/E7FprV6pb+E= -k8s.io/client-go v0.27.2 h1:vDLSeuYvCHKeoQRhCXjxXO45nHVv2Ip4Fe0MfioMrhE= -k8s.io/client-go v0.27.2/go.mod h1:tY0gVmUsHrAmjzHX9zs7eCjxcBsf8IiNe7KQ52biTcQ= -k8s.io/component-base v0.27.2 h1:neju+7s/r5O4x4/txeUONNTS9r1HsPbyoPBAtHsDCpo= -k8s.io/component-base v0.27.2/go.mod h1:5UPk7EjfgrfgRIuDBFtsEFAe4DAvP3U+M8RTzoSJkpo= -k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= -k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f h1:2kWPakN3i/k81b0gvD5C5FJ2kxm1WrQFanWchyKuqGg= -k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f/go.mod h1:byini6yhqGC14c3ebc/QwanvYwhuMWF6yz2F8uwW8eg= -k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= -k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= +k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/apiextensions-apiserver v0.29.0 h1:0VuspFG7Hj+SxyF/Z/2T0uFbI5gb5LRgEyUVE3Q4lV0= +k8s.io/apiextensions-apiserver v0.29.0/go.mod h1:TKmpy3bTS0mr9pylH0nOt/QzQRrW7/h7yLdRForMZwc= +k8s.io/apimachinery v0.29.0 h1:+ACVktwyicPz0oc6MTMLwa2Pw3ouLAfAon1wPLtG48o= +k8s.io/apimachinery v0.29.0/go.mod h1:eVBxQ/cwiJxH58eK/jd/vAk4mrxmVlnpBH5J2GbMeis= +k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= +k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/component-base v0.29.0 h1:T7rjd5wvLnPBV1vC4zWd/iWRbV8Mdxs+nGaoaFzGw3s= +k8s.io/component-base v0.29.0/go.mod h1:sADonFTQ9Zc9yFLghpDpmNXEdHyQmFIGbiuZbqAXQ1M= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.15.1 h1:9UvgKD4ZJGcj24vefUFgZFP3xej/3igL9BsOUTb/+4c= sigs.k8s.io/controller-runtime v0.15.1/go.mod h1:7ngYvp1MLT+9GeZ+6lH3LOlcHkp/+tzA/fmHa4iq9kk= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= -sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/pkg/frr/dbus/dbus.go b/pkg/frr/dbus/dbus.go index f7bf9196..9d826d69 100644 --- a/pkg/frr/dbus/dbus.go +++ b/pkg/frr/dbus/dbus.go @@ -7,7 +7,7 @@ import ( "github.com/coreos/go-systemd/v22/dbus" ) -//go:generate mockgen -destination ./mock/mock_frr.go . System,Connection +//go:generate mockgen -destination ./mock/mock_dbus.go . System,Connection type System interface { NewConn(ctx context.Context) (Connection, error) } diff --git a/pkg/frr/dbus/mock/mock_frr.go b/pkg/frr/dbus/mock/mock_dbus.go similarity index 100% rename from pkg/frr/dbus/mock/mock_frr.go rename to pkg/frr/dbus/mock/mock_dbus.go diff --git a/pkg/frr/manager.go b/pkg/frr/manager.go index ffe31008..cdbf095e 100644 --- a/pkg/frr/manager.go +++ b/pkg/frr/manager.go @@ -11,6 +11,7 @@ import ( "github.com/telekom/das-schiff-network-operator/pkg/config" "github.com/telekom/das-schiff-network-operator/pkg/frr/dbus" + "github.com/telekom/das-schiff-network-operator/pkg/nl" ) const defaultPermissions = 0o640 @@ -20,6 +21,16 @@ var ( frrPermissions = fs.FileMode(defaultPermissions) ) +//go:generate mockgen -destination ./mock/mock_frr.go . ManagerInterface +type ManagerInterface interface { + Init(mgmtVrf string) error + ReloadFRR() error + RestartFRR() error + GetStatusFRR() (activeState, subState string, err error) + Configure(in Configuration, nm *nl.Manager, nwopCfg *config.Config) (bool, error) + SetConfigPath(path string) +} + type Manager struct { configTemplate *template.Template @@ -167,6 +178,10 @@ func (m *Manager) GetStatusFRR() (activeState, subState string, err error) { return activeState, subState, nil } +func (m *Manager) SetConfigPath(path string) { + m.ConfigPath = path +} + func (v *VRFConfiguration) ShouldTemplateVRF() bool { return v.VNI != config.SkipVrfTemplateVni } diff --git a/pkg/frr/mock/mock_frr.go b/pkg/frr/mock/mock_frr.go new file mode 100644 index 00000000..a9738105 --- /dev/null +++ b/pkg/frr/mock/mock_frr.go @@ -0,0 +1,122 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/telekom/das-schiff-network-operator/pkg/frr (interfaces: ManagerInterface) + +// Package mock_frr is a generated GoMock package. +package mock_frr + +import ( + reflect "reflect" + + config "github.com/telekom/das-schiff-network-operator/pkg/config" + frr "github.com/telekom/das-schiff-network-operator/pkg/frr" + nl "github.com/telekom/das-schiff-network-operator/pkg/nl" + gomock "go.uber.org/mock/gomock" +) + +// MockManagerInterface is a mock of ManagerInterface interface. +type MockManagerInterface struct { + ctrl *gomock.Controller + recorder *MockManagerInterfaceMockRecorder +} + +// MockManagerInterfaceMockRecorder is the mock recorder for MockManagerInterface. +type MockManagerInterfaceMockRecorder struct { + mock *MockManagerInterface +} + +// NewMockManagerInterface creates a new mock instance. +func NewMockManagerInterface(ctrl *gomock.Controller) *MockManagerInterface { + mock := &MockManagerInterface{ctrl: ctrl} + mock.recorder = &MockManagerInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockManagerInterface) EXPECT() *MockManagerInterfaceMockRecorder { + return m.recorder +} + +// Configure mocks base method. +func (m *MockManagerInterface) Configure(arg0 frr.Configuration, arg1 *nl.Manager, arg2 *config.Config) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Configure", arg0, arg1, arg2) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Configure indicates an expected call of Configure. +func (mr *MockManagerInterfaceMockRecorder) Configure(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Configure", reflect.TypeOf((*MockManagerInterface)(nil).Configure), arg0, arg1, arg2) +} + +// GetStatusFRR mocks base method. +func (m *MockManagerInterface) GetStatusFRR() (string, string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetStatusFRR") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// GetStatusFRR indicates an expected call of GetStatusFRR. +func (mr *MockManagerInterfaceMockRecorder) GetStatusFRR() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStatusFRR", reflect.TypeOf((*MockManagerInterface)(nil).GetStatusFRR)) +} + +// Init mocks base method. +func (m *MockManagerInterface) Init(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Init", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init. +func (mr *MockManagerInterfaceMockRecorder) Init(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockManagerInterface)(nil).Init), arg0) +} + +// ReloadFRR mocks base method. +func (m *MockManagerInterface) ReloadFRR() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReloadFRR") + ret0, _ := ret[0].(error) + return ret0 +} + +// ReloadFRR indicates an expected call of ReloadFRR. +func (mr *MockManagerInterfaceMockRecorder) ReloadFRR() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadFRR", reflect.TypeOf((*MockManagerInterface)(nil).ReloadFRR)) +} + +// RestartFRR mocks base method. +func (m *MockManagerInterface) RestartFRR() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestartFRR") + ret0, _ := ret[0].(error) + return ret0 +} + +// RestartFRR indicates an expected call of RestartFRR. +func (mr *MockManagerInterfaceMockRecorder) RestartFRR() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestartFRR", reflect.TypeOf((*MockManagerInterface)(nil).RestartFRR)) +} + +// SetConfigPath mocks base method. +func (m *MockManagerInterface) SetConfigPath(arg0 string) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetConfigPath", arg0) +} + +// SetConfigPath indicates an expected call of SetConfigPath. +func (mr *MockManagerInterfaceMockRecorder) SetConfigPath(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetConfigPath", reflect.TypeOf((*MockManagerInterface)(nil).SetConfigPath), arg0) +} diff --git a/pkg/healthcheck/healthcheck.go b/pkg/healthcheck/healthcheck.go index b3084b4c..ec7688de 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -43,8 +43,8 @@ var ( // HealthChecker is a struct that holds data required for networking healthcheck. type HealthChecker struct { - client client.Client - isNetworkingHealthy bool + client client.Client + taintsRemoved bool logr.Logger netConfig *NetHealthcheckConfig toolkit *Toolkit @@ -61,18 +61,18 @@ func NewHealthChecker(clusterClient client.Client, toolkit *Toolkit, netconf *Ne } return &HealthChecker{ - client: clusterClient, - isNetworkingHealthy: false, - Logger: log.Log.WithName("HealthCheck"), - netConfig: netconf, - toolkit: toolkit, - retries: retries, + client: clusterClient, + taintsRemoved: false, + Logger: log.Log.WithName("HealthCheck"), + netConfig: netconf, + toolkit: toolkit, + retries: retries, }, nil } -// IsNetworkingHealthy returns value of isNetworkingHealthly bool. -func (hc *HealthChecker) IsNetworkingHealthy() bool { - return hc.isNetworkingHealthy +// TaintsRemoved returns value of isNetworkingHealthly bool. +func (hc *HealthChecker) TaintsRemoved() bool { + return hc.taintsRemoved } // RemoveTaints removes taint from the node. @@ -102,7 +102,7 @@ func (hc *HealthChecker) RemoveTaints(ctx context.Context) error { } } - hc.isNetworkingHealthy = true + hc.taintsRemoved = true return nil } @@ -137,17 +137,6 @@ func (hc *HealthChecker) CheckInterfaces() error { return nil } -func (hc *HealthChecker) checkInterface(intf string) error { - link, err := hc.toolkit.linkByName(intf) - if err != nil { - return err - } - if link.Attrs().OperState != netlink.OperUp { - return errors.New("link " + intf + " is not up - current state: " + link.Attrs().OperState.String()) - } - return nil -} - // CheckReachability checks if all hosts in Reachability slice are reachable. func (hc *HealthChecker) CheckReachability() error { for _, i := range hc.netConfig.Reachability { @@ -163,6 +152,25 @@ func (hc *HealthChecker) CheckReachability() error { return nil } +// CheckAPIServer checks if Kubernetes Api server is reachable from the pod. +func (hc HealthChecker) CheckAPIServer(ctx context.Context) error { + if err := hc.client.List(ctx, &corev1.NodeList{}); err != nil { + return fmt.Errorf("unable to reach API server: %w", err) + } + return nil +} + +func (hc *HealthChecker) checkInterface(intf string) error { + link, err := hc.toolkit.linkByName(intf) + if err != nil { + return err + } + if link.Attrs().OperState != netlink.OperUp { + return errors.New("link " + intf + " is not up - current state: " + link.Attrs().OperState.String()) + } + return nil +} + func (hc *HealthChecker) checkReachabilityItem(r netReachabilityItem) error { target := r.Host + ":" + strconv.Itoa(r.Port) conn, err := hc.toolkit.tcpDialer.Dial("tcp", target) diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index 2ef639be..074ef408 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -112,10 +112,10 @@ var _ = Describe("RemoveTaints()", func() { hc, err := NewHealthChecker(c, nil, nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.RemoveTaints(context.Background()) Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) }) It("returns error when trying to remove taint (update node)", func() { c := &updateErrorClient{} @@ -123,21 +123,21 @@ var _ = Describe("RemoveTaints()", func() { hc, err := NewHealthChecker(c, nil, nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.RemoveTaints(context.Background()) Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) }) - It("removes taint and set isInitialized true", func() { + It("remove taint and set isInitialized true", func() { c := fake.NewClientBuilder().WithRuntimeObjects(fakeNodes).Build() nc := &NetHealthcheckConfig{} hc, err := NewHealthChecker(c, nil, nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.RemoveTaints(context.Background()) Expect(err).ToNot(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeTrue()) + Expect(hc.TaintsRemoved()).To(BeTrue()) }) }) var _ = Describe("CheckInterfaces()", func() { @@ -147,10 +147,10 @@ var _ = Describe("CheckInterfaces()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeErrorGetByName, &net.Dialer{Timeout: time.Duration(3)}), nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.CheckInterfaces() Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) }) It("returns error if interface is not up", func() { c := fake.NewClientBuilder().Build() @@ -158,10 +158,10 @@ var _ = Describe("CheckInterfaces()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeDownGetByName, &net.Dialer{Timeout: time.Duration(3)}), nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.CheckInterfaces() Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) }) It("returns error if all links are up", func() { c := fake.NewClientBuilder().Build() @@ -169,10 +169,10 @@ var _ = Describe("CheckInterfaces()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("")), nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.CheckInterfaces() Expect(err).ToNot(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) }) }) var _ = Describe("NewTcpDialer()", func() { @@ -181,7 +181,7 @@ var _ = Describe("NewTcpDialer()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("")), &NetHealthcheckConfig{}) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) d := hc.toolkit.tcpDialer.(*net.Dialer) Expect(d.Timeout).To(Equal(time.Second * 3)) }) @@ -190,7 +190,7 @@ var _ = Describe("NewTcpDialer()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("5")), &NetHealthcheckConfig{}) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) d := hc.toolkit.tcpDialer.(*net.Dialer) Expect(d.Timeout).To(Equal(time.Second * 5)) }) @@ -199,7 +199,7 @@ var _ = Describe("NewTcpDialer()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("500ms")), &NetHealthcheckConfig{}) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) d := hc.toolkit.tcpDialer.(*net.Dialer) Expect(d.Timeout).To(Equal(time.Millisecond * 500)) }) @@ -216,7 +216,7 @@ var _ = Describe("CheckReachability()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.CheckReachability() Expect(err).To(HaveOccurred()) }) @@ -229,7 +229,7 @@ var _ = Describe("CheckReachability()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.CheckReachability() Expect(err).ToNot(HaveOccurred()) }) @@ -242,7 +242,7 @@ var _ = Describe("CheckReachability()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.CheckReachability() Expect(err).ToNot(HaveOccurred()) }) @@ -256,11 +256,21 @@ var _ = Describe("CheckReachability()", func() { hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) Expect(err).ToNot(HaveOccurred()) Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + Expect(hc.TaintsRemoved()).To(BeFalse()) err = hc.CheckReachability() Expect(err).To(HaveOccurred()) }) }) +var _ = Describe("CheckAPIServer()", func() { + It("should return no error", func() { + c := fake.NewClientBuilder().Build() + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, nil, nil), &NetHealthcheckConfig{}) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + err = hc.CheckAPIServer(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) +}) func fakeErrorGetByName(_ string) (netlink.Link, error) { return nil, errors.New("Link not found") diff --git a/pkg/managerconfig/managerconfig_test.go b/pkg/managerconfig/managerconfig_test.go index 1ca53f82..c055743e 100644 --- a/pkg/managerconfig/managerconfig_test.go +++ b/pkg/managerconfig/managerconfig_test.go @@ -8,14 +8,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) -var _ = BeforeSuite(func() { - -}) - -func TestHealthCheck(t *testing.T) { +func TestManagerConfig(t *testing.T) { RegisterFailHandler(Fail) RunSpecs(t, - "HealthCheck Suite") + "ManagerConfig Suite") } var _ = Describe("Load()", func() { diff --git a/pkg/reconciler/config_reconciler.go b/pkg/reconciler/config_reconciler.go new file mode 100644 index 00000000..679e9e91 --- /dev/null +++ b/pkg/reconciler/config_reconciler.go @@ -0,0 +1,196 @@ +package reconciler + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/go-logr/logr" + "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/debounce" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + DefaultTimeout = "60s" + DefaultNodeUpdateLimit = 1 +) + +// ConfigReconciler is responsible for creating NetworkConfigRevision objects. +type ConfigReconciler struct { + logger logr.Logger + debouncer *debounce.Debouncer + client client.Client + timeout time.Duration +} + +type reconcileConfig struct { + *ConfigReconciler + logr.Logger +} + +// Reconcile starts reconciliation. +func (cr *ConfigReconciler) Reconcile(ctx context.Context) { + cr.debouncer.Debounce(ctx) +} + +// // NewConfigReconciler creates new reconciler that creates NetworkConfigRevision objects. +func NewConfigReconciler(clusterClient client.Client, logger logr.Logger, timeout time.Duration) (*ConfigReconciler, error) { + reconciler := &ConfigReconciler{ + logger: logger, + timeout: timeout, + client: clusterClient, + } + + reconciler.debouncer = debounce.NewDebouncer(reconciler.ReconcileDebounced, defaultDebounceTime, logger) + + return reconciler, nil +} + +func (cr *ConfigReconciler) ReconcileDebounced(ctx context.Context) error { + r := &reconcileConfig{ + ConfigReconciler: cr, + Logger: cr.logger, + } + + cr.logger.Info("fetching config data...") + + timeoutCtx, cancel := context.WithTimeout(ctx, cr.timeout) + defer cancel() + + // get VRFRouteConfiguration, Layer2networkConfiguration and RoutingTable objects + configData, err := r.fetchConfigData(timeoutCtx) + if err != nil { + return fmt.Errorf("failed to fetch configuration details: %w", err) + } + + // prepare new revision + revision, err := v1alpha1.NewRevision(configData) + if err != nil { + return fmt.Errorf("failed to prepare new NetworkConfigRevision: %w", err) + } + + r.logger.Info("new NetworkConfigRevision prepared", "name", revision.Name) + + // get all known revisions + revisions, err := listRevisions(timeoutCtx, cr.client) + if err != nil { + return fmt.Errorf("failed to list NetworkConfigRevisions: %w", err) + } + + // check if revision should be skipped (e.g. it is the same as known invalid revision, or as currently deployed revision) + if cr.shouldSkip(revisions, revision) { + return nil + } + + // create revision object + if err := r.createRevision(timeoutCtx, revision); err != nil { + return fmt.Errorf("faild to create NetworkConfigRevision %s: %w", revision.Name, err) + } + + cr.logger.Info("deployed NetworkConfigRevision", "name", revision.Name) + return nil +} + +func (cr *ConfigReconciler) shouldSkip(revisions *v1alpha1.NetworkConfigRevisionList, processedRevision *v1alpha1.NetworkConfigRevision) bool { + if len(revisions.Items) > 0 && revisions.Items[0].Spec.Revision == processedRevision.Spec.Revision { + cr.logger.Info("NetworkConfigRevision creation aborted - new revision equals to the last known one") + // new NetworkConfigRevision equals to the last known one - skip (no update is required) + return true + } + + for i := range revisions.Items { + if !revisions.Items[i].Status.IsInvalid { + if revisions.Items[i].Spec.Revision == processedRevision.Spec.Revision { + cr.logger.Info("NetworkConfigRevision creation aborted - new revision equals to the last known valid one") + // new NetworkConfigRevision equals to the last known valid one - skip (should be already deployed) + return true + } + break + } + } + + for i := range revisions.Items { + if (revisions.Items[i].Spec.Revision == processedRevision.Spec.Revision) && revisions.Items[i].Status.IsInvalid { + // new NetworkConfigRevision is equal to known invalid revision - skip + cr.logger.Info("NetworkConfigRevision creation aborted - new revision is equal to known invalid revision") + return true + } + } + + return false +} + +func (r *reconcileConfig) createRevision(ctx context.Context, revision *v1alpha1.NetworkConfigRevision) error { + r.logger.Info("creating NetworkConfigRevision", "name", revision.Name) + if err := r.client.Create(ctx, revision); err != nil { + if !apierrors.IsAlreadyExists(err) { + return fmt.Errorf("error creating NodeConfigRevision: %w", err) + } + if err := r.client.Delete(ctx, revision); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("error deleting old instance of revision %s: %w", revision.Name, err) + } + if err := r.client.Create(ctx, revision); err != nil { + return fmt.Errorf("error creating new instance of revision %s: %w", revision.Name, err) + } + } + return nil +} + +func (r *reconcileConfig) fetchConfigData(ctx context.Context) (*v1alpha1.NodeNetworkConfig, error) { + // get VRFRouteConfiguration objects + l3vnis, err := r.fetchLayer3(ctx) + if err != nil { + return nil, err + } + + // get Layer2networkConfiguration objects + l2vnis, err := r.fetchLayer2(ctx) + if err != nil { + return nil, err + } + + // get RoutingTable objects + taas, err := r.fetchTaas(ctx) + if err != nil { + return nil, err + } + + config := &v1alpha1.NodeNetworkConfig{} + + // discard metadata from previously fetched objects + config.Spec.Layer2 = []v1alpha1.Layer2NetworkConfigurationSpec{} + for i := range l2vnis { + config.Spec.Layer2 = append(config.Spec.Layer2, l2vnis[i].Spec) + } + + config.Spec.Vrf = []v1alpha1.VRFRouteConfigurationSpec{} + for i := range l3vnis { + config.Spec.Vrf = append(config.Spec.Vrf, l3vnis[i].Spec) + } + + config.Spec.RoutingTable = []v1alpha1.RoutingTableSpec{} + for i := range taas { + config.Spec.RoutingTable = append(config.Spec.RoutingTable, taas[i].Spec) + } + + return config, nil +} + +func listRevisions(ctx context.Context, c client.Client) (*v1alpha1.NetworkConfigRevisionList, error) { + revisions := &v1alpha1.NetworkConfigRevisionList{} + if err := c.List(ctx, revisions); err != nil { + return nil, fmt.Errorf("error listing NetworkConfigRevisions: %w", err) + } + + // sort revisions by creation date ascending (newest first) + if len(revisions.Items) > 0 { + slices.SortFunc(revisions.Items, func(a, b v1alpha1.NetworkConfigRevision) int { + return b.GetCreationTimestamp().Compare(a.GetCreationTimestamp().Time) // newest first + }) + } + + return revisions, nil +} diff --git a/pkg/reconciler/configrevision_reconciler.go b/pkg/reconciler/configrevision_reconciler.go new file mode 100644 index 00000000..44a52d54 --- /dev/null +++ b/pkg/reconciler/configrevision_reconciler.go @@ -0,0 +1,471 @@ +package reconciler + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + "time" + + "github.com/go-logr/logr" + "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/debounce" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +const ( + StatusInvalid = "invalid" + StatusProvisioning = "provisioning" + StatusProvisioned = "provisioned" + + DefaultConfigTimeout = "2m" + DefaultPreconfigTimout = "10m" + + controlPlaneLabel = "node-role.kubernetes.io/control-plane" + numOfRefs = 2 + + numOfDeploymentRetries = 3 +) + +// ConfigRevisionReconciler is responsible for creating NodeConfig objects. +type ConfigRevisionReconciler struct { + logger logr.Logger + debouncer *debounce.Debouncer + client client.Client + apiTimeout time.Duration + configTimeout time.Duration + preconfigTimeout time.Duration + scheme *runtime.Scheme + maxUpdating int +} + +// Reconcile starts reconciliation. +func (crr *ConfigRevisionReconciler) Reconcile(ctx context.Context) { + crr.debouncer.Debounce(ctx) +} + +// // NewNodeConfigReconciler creates new reconciler that creates NodeConfig objects. +func NewNodeConfigReconciler(clusterClient client.Client, logger logr.Logger, apiTimeout, configTimeout, preconfigTimeout time.Duration, s *runtime.Scheme, maxUpdating int) (*ConfigRevisionReconciler, error) { + reconciler := &ConfigRevisionReconciler{ + logger: logger, + apiTimeout: apiTimeout, + configTimeout: configTimeout, + preconfigTimeout: preconfigTimeout, + client: clusterClient, + scheme: s, + maxUpdating: maxUpdating, + } + + reconciler.debouncer = debounce.NewDebouncer(reconciler.reconcileDebounced, defaultDebounceTime, logger) + + return reconciler, nil +} + +func (crr *ConfigRevisionReconciler) reconcileDebounced(ctx context.Context) error { + revisions, err := listRevisions(ctx, crr.client) + if err != nil { + return fmt.Errorf("error listing revisions: %w", err) + } + + nodes, err := listNodes(ctx, crr.client) + if err != nil { + return fmt.Errorf("error listing nodes: %w", err) + } + + nodeConfigs, err := crr.listConfigs(ctx) + if err != nil { + return fmt.Errorf("error listing configs: %w", err) + } + + totalNodes := len(nodes) + cntMap := map[string]*counters{} + for i := range revisions.Items { + var cnt *counters + var err error + if cnt, err = crr.processConfigsForRevision(ctx, nodeConfigs.Items, &revisions.Items[i]); err != nil { + return fmt.Errorf("failed to process configs for revision %s: %w", revisions.Items[i].Name, err) + } + cntMap[revisions.Items[i].Spec.Revision] = cnt + } + + revisionToDeploy := getFirstValidRevision(revisions.Items) + + nodesToDeploy := getOutdatedNodes(nodes, nodeConfigs.Items, revisionToDeploy) + + if err := crr.updateRevisionCounters(ctx, revisions.Items, revisionToDeploy, len(nodesToDeploy), totalNodes, cntMap); err != nil { + return fmt.Errorf("failed to update queue counters: %w", err) + } + + // there is nothing to deploy - skip + if revisionToDeploy == nil { + crr.logger.Error(fmt.Errorf("there is no revision to deploy"), "revision deployment aboorted") + return nil + } + + if revisionToDeploy.Status.Ongoing < crr.maxUpdating && len(nodesToDeploy) > 0 { + if err := crr.deployNodeConfig(ctx, nodesToDeploy[0], revisionToDeploy); err != nil { + return fmt.Errorf("error deploying node configurations: %w", err) + } + } + + // remove all but last known valid revision + if err := crr.revisionCleanup(ctx); err != nil { + return fmt.Errorf("error cleaning redundant revisions: %w", err) + } + + return nil +} + +func getFirstValidRevision(revisions []v1alpha1.NetworkConfigRevision) *v1alpha1.NetworkConfigRevision { + i := slices.IndexFunc(revisions, func(r v1alpha1.NetworkConfigRevision) bool { + return !r.Status.IsInvalid + }) + if i > -1 { + return &revisions[i] + } + return nil +} + +type counters struct { + ready, ongoing, invalid int +} + +func (crr *ConfigRevisionReconciler) processConfigsForRevision(ctx context.Context, configs []v1alpha1.NodeNetworkConfig, revision *v1alpha1.NetworkConfigRevision) (*counters, error) { + configs, err := crr.removeRedundantConfigs(ctx, configs) + if err != nil { + return nil, fmt.Errorf("failed to remove redundant configs: %w", err) + } + ready, ongoing, invalid := crr.getRevisionCounters(configs, revision) + cnt := &counters{ready: ready, ongoing: ongoing, invalid: invalid} + + if invalid > 0 { + if err := crr.invalidateRevision(ctx, revision, "NetworkConfigRevision results in invalid config"); err != nil { + return cnt, fmt.Errorf("faild to invalidate revision %s: %w", revision.Name, err) + } + } + + return cnt, nil +} + +func (crr *ConfigRevisionReconciler) getRevisionCounters(configs []v1alpha1.NodeNetworkConfig, revision *v1alpha1.NetworkConfigRevision) (ready, ongoing, invalid int) { + ready = 0 + ongoing = 0 + invalid = 0 + for i := range configs { + if configs[i].Spec.Revision == revision.Spec.Revision { + timeout := crr.configTimeout + switch configs[i].Status.ConfigStatus { + case StatusProvisioned: + // Update ready counter + ready++ + case StatusInvalid: + // Increase 'invalid' counter so we know that the revision results in invalid configs + invalid++ + case "": + // Set longer timeout if status was not yet updated + timeout = crr.preconfigTimeout + fallthrough + case StatusProvisioning: + // Update ongoing counter + ongoing++ + if wasConfigTimeoutReached(&configs[i], timeout) { + // If timout was reached revision is invalid (but still counts as ongoing). + invalid++ + } + } + } + } + return ready, ongoing, invalid +} + +func (crr *ConfigRevisionReconciler) removeRedundantConfigs(ctx context.Context, configs []v1alpha1.NodeNetworkConfig) ([]v1alpha1.NodeNetworkConfig, error) { + cfg := []v1alpha1.NodeNetworkConfig{} + for i := range configs { + // Every NodeNetworkConfig obejct should have 2 owner references - for NodeConfigRevision and for the Node. If there is only one owner reference, + // it means that either node or revision were deleted, so the config itself can be deleted as well. + if len(configs[i].ObjectMeta.OwnerReferences) < numOfRefs { + crr.logger.Info("deleting redundant NodeNetworkConfig", "name", configs[i].Name) + if err := crr.client.Delete(ctx, &configs[i]); err != nil && !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("error deleting redundant node config - %s: %w", configs[i].Name, err) + } + } else { + cfg = append(cfg, configs[i]) + } + } + return cfg, nil +} + +func (crr *ConfigRevisionReconciler) invalidateRevision(ctx context.Context, revision *v1alpha1.NetworkConfigRevision, reason string) error { + crr.logger.Info("invalidating revision", "name", revision.Name, "reason", reason) + revision.Status.IsInvalid = true + if err := crr.client.Status().Update(ctx, revision); err != nil { + return fmt.Errorf("failed to update revision status %s: %w", revision.Name, err) + } + return nil +} + +func wasConfigTimeoutReached(cfg *v1alpha1.NodeNetworkConfig, timeout time.Duration) bool { + if cfg.Status.LastUpdate.IsZero() { + return false + } + return time.Now().After(cfg.Status.LastUpdate.Add(timeout)) +} + +func getOutdatedNodes(nodes map[string]*corev1.Node, configs []v1alpha1.NodeNetworkConfig, revision *v1alpha1.NetworkConfigRevision) []*corev1.Node { + if revision == nil { + return []*corev1.Node{} + } + + for nodeName := range nodes { + for i := range configs { + if configs[i].Name == nodeName && configs[i].Spec.Revision == revision.Spec.Revision { + delete(nodes, nodeName) + break + } + } + } + + nodesToDeploy := []*corev1.Node{} + for _, node := range nodes { + nodesToDeploy = append(nodesToDeploy, node) + } + return nodesToDeploy +} + +func (crr *ConfigRevisionReconciler) updateRevisionCounters(ctx context.Context, revisions []v1alpha1.NetworkConfigRevision, currentRevision *v1alpha1.NetworkConfigRevision, queued, totalNodes int, cnt map[string]*counters) error { + for i := range revisions { + q := 0 + if currentRevision != nil && revisions[i].Spec.Revision == currentRevision.Spec.Revision { + q = queued + } + revisions[i].Status.Queued = q + revisions[i].Status.Ongoing = cnt[revisions[i].Spec.Revision].ongoing + revisions[i].Status.Ready = cnt[revisions[i].Spec.Revision].ready + revisions[i].Status.Total = totalNodes + if err := crr.client.Status().Update(ctx, &revisions[i]); err != nil { + return fmt.Errorf("failed to update counters for revision %s: %w", revisions[i].Name, err) + } + } + return nil +} + +func (crr *ConfigRevisionReconciler) revisionCleanup(ctx context.Context) error { + revisions, err := listRevisions(ctx, crr.client) + if err != nil { + return fmt.Errorf("failed to list revisions: %w", err) + } + + if len(revisions.Items) > 1 { + nodeConfigs, err := crr.listConfigs(ctx) + if err != nil { + return fmt.Errorf("failed to list configs: %w", err) + } + if !revisions.Items[0].Status.IsInvalid && revisions.Items[0].Status.Ready == revisions.Items[0].Status.Total { + for i := 1; i < len(revisions.Items); i++ { + if countReferences(&revisions.Items[i], nodeConfigs.Items) == 0 { + crr.logger.Info("deleting NetworkConfigRevision", "name", revisions.Items[i].Name) + if err := crr.client.Delete(ctx, &revisions.Items[i]); err != nil { + return fmt.Errorf("failed to delete revision %s: %w", revisions.Items[i].Name, err) + } + } + } + } + } + + return nil +} + +func countReferences(revision *v1alpha1.NetworkConfigRevision, configs []v1alpha1.NodeNetworkConfig) int { + refCnt := 0 + for j := range configs { + if configs[j].Spec.Revision == revision.Spec.Revision { + refCnt++ + } + } + return refCnt +} + +func (crr *ConfigRevisionReconciler) listConfigs(ctx context.Context) (*v1alpha1.NodeNetworkConfigList, error) { + nodeConfigs := &v1alpha1.NodeNetworkConfigList{} + if err := crr.client.List(ctx, nodeConfigs); err != nil { + return nil, fmt.Errorf("error listing NodeNetworkConfigs: %w", err) + } + return nodeConfigs, nil +} + +func (crr *ConfigRevisionReconciler) deployNodeConfig(ctx context.Context, node *corev1.Node, revision *v1alpha1.NetworkConfigRevision) error { + currentConfig := &v1alpha1.NodeNetworkConfig{} + if err := crr.client.Get(ctx, types.NamespacedName{Name: node.Name}, currentConfig); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("error getting NodeNetworkConfig object for node %s: %w", node.Name, err) + } + currentConfig = nil + } + + if currentConfig != nil && currentConfig.Spec.Revision == revision.Spec.Revision { + // current config is the same as current revision - skip + return nil + } + + newConfig, err := crr.createConfigForNode(node, revision) + if err != nil { + return fmt.Errorf("error preparing NodeNetworkConfig for node %s: %w", node.Name, err) + } + + for i := 0; i < numOfDeploymentRetries; i++ { + if err := crr.deployConfig(ctx, newConfig, currentConfig, node); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + continue + } + return fmt.Errorf("error deploying NodeNetworkConfig for node %s: %w", node.Name, err) + } + break + } + + crr.logger.Info("deployed NodeNetworkConfig", "name", newConfig.Name) + + return nil +} + +func (crr *ConfigRevisionReconciler) createConfigForNode(node *corev1.Node, revision *v1alpha1.NetworkConfigRevision) (*v1alpha1.NodeNetworkConfig, error) { + // create new config + c := &v1alpha1.NodeNetworkConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: node.Name, + }, + } + + c.Spec = revision.Spec.Config + c.Spec.Revision = revision.Spec.Revision + c.Name = node.Name + + if err := controllerutil.SetOwnerReference(node, c, scheme.Scheme); err != nil { + return nil, fmt.Errorf("error setting owner references (node): %w", err) + } + + if err := controllerutil.SetOwnerReference(revision, c, crr.scheme); err != nil { + return nil, fmt.Errorf("error setting owner references (revision): %w", err) + } + + // prepare Layer2NetworkConfigurationSpec (l2Spec) for each node. + // Each Layer2NetworkConfigurationSpec from l2Spec has node selector, + // which should be used to add config to proper nodes. + // Each Layer2NetworkConfigurationSpec that don't match the node selector + // is removed. + var err error + c.Spec.Layer2 = slices.DeleteFunc(c.Spec.Layer2, func(s v1alpha1.Layer2NetworkConfigurationSpec) bool { + if err != nil { + // skip if any errors occurred + return false + } + if s.NodeSelector == nil { + // node selector is not defined for the spec. + // Layer2 is global - just continue + return false + } + + // node selector of type v1.labelSelector has to be converted + // to labels.Selector type to be used with controller-runtime client + var selector labels.Selector + selector, err = convertSelector(s.NodeSelector.MatchLabels, s.NodeSelector.MatchExpressions) + if err != nil { + return false + } + + // remove currently processed Layer2NetworkConfigurationSpec if node does not match the selector + return !selector.Matches(labels.Set(node.ObjectMeta.Labels)) + }) + + if err != nil { + return nil, fmt.Errorf("failed to delete redundant Layer2NetworkConfigurationSpec: %w", err) + } + + // set config as next config for the node + return c, nil +} + +func convertSelector(matchLabels map[string]string, matchExpressions []metav1.LabelSelectorRequirement) (labels.Selector, error) { + selector := labels.NewSelector() + var reqs labels.Requirements + + for key, value := range matchLabels { + requirement, err := labels.NewRequirement(key, selection.Equals, []string{value}) + if err != nil { + return nil, fmt.Errorf("error creating MatchLabel requirement: %w", err) + } + reqs = append(reqs, *requirement) + } + + for _, req := range matchExpressions { + lowercaseOperator := selection.Operator(strings.ToLower(string(req.Operator))) + requirement, err := labels.NewRequirement(req.Key, lowercaseOperator, req.Values) + if err != nil { + return nil, fmt.Errorf("error creating MatchExpression requirement: %w", err) + } + reqs = append(reqs, *requirement) + } + selector = selector.Add(reqs...) + + return selector, nil +} + +func (crr *ConfigRevisionReconciler) deployConfig(ctx context.Context, newConfig, currentConfig *v1alpha1.NodeNetworkConfig, node *corev1.Node) error { + deploymentCtx, deploymentCtxCancel := context.WithTimeout(ctx, crr.apiTimeout) + defer deploymentCtxCancel() + var cfg *v1alpha1.NodeNetworkConfig + if currentConfig != nil { + cfg = currentConfig + // there already is config for node - update + cfg.Spec = newConfig.Spec + cfg.ObjectMeta.OwnerReferences = newConfig.ObjectMeta.OwnerReferences + cfg.Name = node.Name + if err := crr.client.Update(deploymentCtx, cfg); err != nil { + return fmt.Errorf("error updating NodeNetworkConfig for node %s: %w", node.Name, err) + } + } else { + cfg = newConfig + // there is no config for node - create one + if err := crr.client.Create(deploymentCtx, cfg); err != nil { + return fmt.Errorf("error creating NodeNetworkConfig for node %s: %w", node.Name, err) + } + } + + return nil +} + +func listNodes(ctx context.Context, c client.Client) (map[string]*corev1.Node, error) { + // list all nodes + list := &corev1.NodeList{} + if err := c.List(ctx, list); err != nil { + return nil, fmt.Errorf("unable to list nodes: %w", err) + } + + // discard control-plane and not-ready nodes + nodes := map[string]*corev1.Node{} + for i := range list.Items { + _, isControlPlane := list.Items[i].Labels[controlPlaneLabel] + if !isControlPlane { + // discard nodes that are not in ready state + for j := range list.Items[i].Status.Conditions { + // TODO: Should taint node.kubernetes.io/not-ready be used instead of Conditions? + if list.Items[i].Status.Conditions[j].Type == corev1.NodeReady && + list.Items[i].Status.Conditions[j].Status == corev1.ConditionTrue { + nodes[list.Items[i].Name] = &list.Items[i] + break + } + } + } + } + + return nodes, nil +} diff --git a/pkg/reconciler/layer2.go b/pkg/reconciler/layer2.go index 941b28f6..49a26850 100644 --- a/pkg/reconciler/layer2.go +++ b/pkg/reconciler/layer2.go @@ -4,19 +4,12 @@ import ( "context" "fmt" "net" - "os" - "strings" networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" - "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" "github.com/telekom/das-schiff-network-operator/pkg/nl" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" - "k8s.io/apimachinery/pkg/types" ) -func (r *reconcile) fetchLayer2(ctx context.Context) ([]networkv1alpha1.Layer2NetworkConfiguration, error) { +func (r *reconcileConfig) fetchLayer2(ctx context.Context) ([]networkv1alpha1.Layer2NetworkConfiguration, error) { layer2List := &networkv1alpha1.Layer2NetworkConfigurationList{} err := r.client.List(ctx, layer2List) if err != nil { @@ -24,59 +17,17 @@ func (r *reconcile) fetchLayer2(ctx context.Context) ([]networkv1alpha1.Layer2Ne return nil, fmt.Errorf("error getting list of Layer2s from Kubernetes: %w", err) } - nodeName := os.Getenv(healthcheck.NodenameEnv) - node := &corev1.Node{} - err = r.client.Get(ctx, types.NamespacedName{Name: nodeName}, node) - if err != nil { - r.Logger.Error(err, "error getting local node name") - return nil, fmt.Errorf("error getting local node name: %w", err) - } - l2vnis := []networkv1alpha1.Layer2NetworkConfiguration{} - for i := range layer2List.Items { - item := &layer2List.Items[i] - logger := r.Logger.WithValues("name", item.ObjectMeta.Name, "namespace", item.ObjectMeta.Namespace, "vlan", item.Spec.ID, "vni", item.Spec.VNI) - if item.Spec.NodeSelector != nil { - selector := labels.NewSelector() - var reqs labels.Requirements - - for key, value := range item.Spec.NodeSelector.MatchLabels { - requirement, err := labels.NewRequirement(key, selection.Equals, []string{value}) - if err != nil { - logger.Error(err, "error creating MatchLabel requirement") - return nil, fmt.Errorf("error creating MatchLabel requirement: %w", err) - } - reqs = append(reqs, *requirement) - } - - for _, req := range item.Spec.NodeSelector.MatchExpressions { - lowercaseOperator := selection.Operator(strings.ToLower(string(req.Operator))) - requirement, err := labels.NewRequirement(req.Key, lowercaseOperator, req.Values) - if err != nil { - logger.Error(err, "error creating MatchExpression requirement") - return nil, fmt.Errorf("error creating MatchExpression requirement: %w", err) - } - reqs = append(reqs, *requirement) - } - selector = selector.Add(reqs...) - - if !selector.Matches(labels.Set(node.ObjectMeta.Labels)) { - logger.Info("local node does not match nodeSelector of layer2", "node", nodeName) - continue - } - } - - l2vnis = append(l2vnis, *item) - } + l2vnis = append(l2vnis, layer2List.Items...) - if err := r.checkL2Duplicates(l2vnis); err != nil { + if err := checkL2Duplicates(l2vnis); err != nil { return nil, err } return l2vnis, nil } -func (r *reconcile) reconcileLayer2(l2vnis []networkv1alpha1.Layer2NetworkConfiguration) error { +func (r *reconcileNodeNetworkConfig) reconcileLayer2(l2vnis []networkv1alpha1.Layer2NetworkConfigurationSpec) error { desired, err := r.getDesired(l2vnis) if err != nil { return err @@ -129,7 +80,7 @@ func (r *reconcile) reconcileLayer2(l2vnis []networkv1alpha1.Layer2NetworkConfig return nil } -func (r *reconcile) createL2(info *nl.Layer2Information, anycastTrackerInterfaces *[]int) error { +func (r *reconcileNodeNetworkConfig) createL2(info *nl.Layer2Information, anycastTrackerInterfaces *[]int) error { r.Logger.Info("Creating Layer2", "vlan", info.VlanID, "vni", info.VNI) err := r.netlinkManager.CreateL2(info) if err != nil { @@ -145,7 +96,7 @@ func (r *reconcile) createL2(info *nl.Layer2Information, anycastTrackerInterface return nil } -func (r *reconcile) getDesired(l2vnis []networkv1alpha1.Layer2NetworkConfiguration) ([]nl.Layer2Information, error) { +func (r *reconcileNodeNetworkConfig) getDesired(l2vnis []networkv1alpha1.Layer2NetworkConfigurationSpec) ([]nl.Layer2Information, error) { availableVrfs, err := r.netlinkManager.ListL3() if err != nil { return nil, fmt.Errorf("error loading available VRFs: %w", err) @@ -153,7 +104,7 @@ func (r *reconcile) getDesired(l2vnis []networkv1alpha1.Layer2NetworkConfigurati desired := []nl.Layer2Information{} for i := range l2vnis { - spec := l2vnis[i].Spec + spec := l2vnis[i] var anycastMAC *net.HardwareAddr if mac, err := net.ParseMAC(spec.AnycastMac); err == nil { @@ -162,7 +113,7 @@ func (r *reconcile) getDesired(l2vnis []networkv1alpha1.Layer2NetworkConfigurati anycastGateways, err := r.netlinkManager.ParseIPAddresses(spec.AnycastGateways) if err != nil { - r.Logger.Error(err, "error parsing anycast gateways", "layer", l2vnis[i].ObjectMeta.Name, "gw", spec.AnycastGateways) + r.Logger.Error(err, "error parsing anycast gateways", "gw", spec.AnycastGateways) return nil, fmt.Errorf("error parsing anycast gateways: %w", err) } @@ -175,7 +126,7 @@ func (r *reconcile) getDesired(l2vnis []networkv1alpha1.Layer2NetworkConfigurati } } if !vrfAvailable { - r.Logger.Error(err, "VRF of Layer2 not found on node", "layer", l2vnis[i].ObjectMeta.Name, "vrf", spec.VRF) + r.Logger.Error(err, "VRF of Layer2 not found on node", "vrf", spec.VRF) continue } } @@ -213,7 +164,7 @@ func determineToBeDeleted(existing, desired []nl.Layer2Information) []nl.Layer2I return toDelete } -func (r *reconcile) reconcileExistingLayer(desired, currentConfig *nl.Layer2Information, anycastTrackerInterfaces *[]int) error { +func (r *reconcileNodeNetworkConfig) reconcileExistingLayer(desired, currentConfig *nl.Layer2Information, anycastTrackerInterfaces *[]int) error { r.Logger.Info("Reconciling existing Layer2", "vlan", desired.VlanID, "vni", desired.VNI) err := r.netlinkManager.ReconcileL2(currentConfig, desired) if err != nil { @@ -229,7 +180,7 @@ func (r *reconcile) reconcileExistingLayer(desired, currentConfig *nl.Layer2Info return nil } -func (*reconcile) checkL2Duplicates(configs []networkv1alpha1.Layer2NetworkConfiguration) error { +func checkL2Duplicates(configs []networkv1alpha1.Layer2NetworkConfiguration) error { for i := range configs { for j := i + 1; j < len(configs); j++ { if configs[i].Spec.ID == configs[j].Spec.ID { diff --git a/pkg/reconciler/layer3.go b/pkg/reconciler/layer3.go index ecad4bfe..9630df86 100644 --- a/pkg/reconciler/layer3.go +++ b/pkg/reconciler/layer3.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net" + "os" "sort" "strconv" "time" @@ -12,12 +13,14 @@ import ( networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" "github.com/telekom/das-schiff-network-operator/pkg/config" "github.com/telekom/das-schiff-network-operator/pkg/frr" + "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" "github.com/telekom/das-schiff-network-operator/pkg/nl" + "k8s.io/apimachinery/pkg/types" ) const defaultSleep = 2 * time.Second -func (r *reconcile) fetchLayer3(ctx context.Context) ([]networkv1alpha1.VRFRouteConfiguration, error) { +func (r *reconcileConfig) fetchLayer3(ctx context.Context) ([]networkv1alpha1.VRFRouteConfiguration, error) { vrfs := &networkv1alpha1.VRFRouteConfigurationList{} err := r.client.List(ctx, vrfs) if err != nil { @@ -28,7 +31,7 @@ func (r *reconcile) fetchLayer3(ctx context.Context) ([]networkv1alpha1.VRFRoute return vrfs.Items, nil } -func (r *reconcile) fetchTaas(ctx context.Context) ([]networkv1alpha1.RoutingTable, error) { +func (r *reconcileConfig) fetchTaas(ctx context.Context) ([]networkv1alpha1.RoutingTable, error) { tables := &networkv1alpha1.RoutingTableList{} err := r.client.List(ctx, tables) if err != nil { @@ -39,8 +42,17 @@ func (r *reconcile) fetchTaas(ctx context.Context) ([]networkv1alpha1.RoutingTab return tables.Items, nil } +func (r *reconcileNodeNetworkConfig) fetchNodeConfig(ctx context.Context) (*networkv1alpha1.NodeNetworkConfig, error) { + cfg := &networkv1alpha1.NodeNetworkConfig{} + err := r.client.Get(ctx, types.NamespacedName{Name: os.Getenv(healthcheck.NodenameEnv)}, cfg) + if err != nil { + return nil, fmt.Errorf("error getting NodeConfig: %w", err) + } + return cfg, nil +} + // nolint: contextcheck // context is not relevant -func (r *reconcile) reconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfiguration, taas []networkv1alpha1.RoutingTable) error { +func (r *reconcileNodeNetworkConfig) reconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfigurationSpec, taas []networkv1alpha1.RoutingTableSpec) error { vrfConfigMap, err := r.createVrfConfigMap(l3vnis) if err != nil { return err @@ -105,7 +117,7 @@ func (r *reconcile) reconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfigurati return nil } -func (r *reconcile) configureFRR(vrfConfigs []frr.VRFConfiguration) error { +func (r *reconcileNodeNetworkConfig) configureFRR(vrfConfigs []frr.VRFConfiguration) error { changed, err := r.frrManager.Configure(frr.Configuration{ VRFs: vrfConfigs, ASN: r.config.ServerASN, @@ -123,7 +135,7 @@ func (r *reconcile) configureFRR(vrfConfigs []frr.VRFConfiguration) error { return nil } -func (r *reconcile) reloadFRR() error { +func (r *reconcileNodeNetworkConfig) reloadFRR() error { r.Logger.Info("trying to reload FRR config because it changed") err := r.frrManager.ReloadFRR() if err != nil { @@ -139,11 +151,11 @@ func (r *reconcile) reloadFRR() error { return nil } -func (r *reconcile) createVrfConfigMap(l3vnis []networkv1alpha1.VRFRouteConfiguration) (map[string]frr.VRFConfiguration, error) { +func (r *reconcileNodeNetworkConfig) createVrfConfigMap(l3vnis []networkv1alpha1.VRFRouteConfigurationSpec) (map[string]frr.VRFConfiguration, error) { vrfConfigMap := map[string]frr.VRFConfiguration{} for i := range l3vnis { - spec := l3vnis[i].Spec - logger := r.Logger.WithValues("name", l3vnis[i].ObjectMeta.Name, "namespace", l3vnis[i].ObjectMeta.Namespace, "vrf", spec.VRF) + spec := l3vnis[i] + logger := r.Logger.WithValues("vrf", spec.VRF) var vni int var rt string @@ -159,13 +171,13 @@ func (r *reconcile) createVrfConfigMap(l3vnis []networkv1alpha1.VRFRouteConfigur vni = config.SkipVrfTemplateVni } else { err := fmt.Errorf("vrf not in vrf vni map") - r.Logger.Error(err, "VRF does not exist in VRF VNI config, ignoring", "vrf", spec.VRF, "name", l3vnis[i].ObjectMeta.Name, "namespace", l3vnis[i].ObjectMeta.Namespace) + r.Logger.Error(err, "VRF does not exist in VRF VNI config, ignoring", "vrf", spec.VRF) continue } if vni == 0 && vni > 16777215 { err := fmt.Errorf("VNI can not be set to 0") - r.Logger.Error(err, "VNI can not be set to 0, ignoring", "vrf", spec.VRF, "name", l3vnis[i].ObjectMeta.Name, "namespace", l3vnis[i].ObjectMeta.Namespace) + r.Logger.Error(err, "VNI can not be set to 0, ignoring", "vrf", spec.VRF) continue } @@ -179,11 +191,11 @@ func (r *reconcile) createVrfConfigMap(l3vnis []networkv1alpha1.VRFRouteConfigur return vrfConfigMap, nil } -func createVrfFromTaaS(taas []networkv1alpha1.RoutingTable) map[string]frr.VRFConfiguration { +func createVrfFromTaaS(taas []networkv1alpha1.RoutingTableSpec) map[string]frr.VRFConfiguration { vrfConfigMap := map[string]frr.VRFConfiguration{} for i := range taas { - spec := taas[i].Spec + spec := taas[i] name := fmt.Sprintf("taas.%d", spec.TableID) @@ -238,7 +250,7 @@ func createVrfConfig(vrfConfigMap map[string]frr.VRFConfiguration, spec *network return &cfg, nil } -func (r *reconcile) reconcileL3Netlink(vrfConfigs []frr.VRFConfiguration) ([]nl.VRFInformation, bool, error) { +func (r *reconcileNodeNetworkConfig) reconcileL3Netlink(vrfConfigs []frr.VRFConfiguration) ([]nl.VRFInformation, bool, error) { existing, err := r.netlinkManager.ListL3() if err != nil { return nil, false, fmt.Errorf("error listing L3 VRF information: %w", err) @@ -277,7 +289,7 @@ func (r *reconcile) reconcileL3Netlink(vrfConfigs []frr.VRFConfiguration) ([]nl. return toCreate, len(toDelete) > 0, nil } -func (r *reconcile) gatherInterfacesInfo(vrfConfigs []frr.VRFConfiguration, existing []nl.VRFInformation) (preexisting, toDelete []nl.VRFInformation) { +func (r *reconcileNodeNetworkConfig) gatherInterfacesInfo(vrfConfigs []frr.VRFConfiguration, existing []nl.VRFInformation) (preexisting, toDelete []nl.VRFInformation) { // Check for VRFs that are configured on the host but no longer in Kubernetes for i := range existing { stillExists := false @@ -301,7 +313,7 @@ func (r *reconcile) gatherInterfacesInfo(vrfConfigs []frr.VRFConfiguration, exis return preexisting, toDelete } -func (r *reconcile) reconcileTaasNetlink(vrfConfigs []frr.VRFConfiguration) (bool, error) { +func (r *reconcileNodeNetworkConfig) reconcileTaasNetlink(vrfConfigs []frr.VRFConfiguration) (bool, error) { existing, err := r.netlinkManager.ListTaas() if err != nil { return false, fmt.Errorf("error listing TaaS VRF information: %w", err) @@ -320,7 +332,7 @@ func (r *reconcile) reconcileTaasNetlink(vrfConfigs []frr.VRFConfiguration) (boo return deletedInterface, nil } -func (r *reconcile) cleanupTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) (bool, error) { +func (r *reconcileNodeNetworkConfig) cleanupTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) (bool, error) { deletedInterface := false for _, cfg := range existing { stillExists := false @@ -340,7 +352,7 @@ func (r *reconcile) cleanupTaasNetlink(existing []nl.TaasInformation, intended [ return deletedInterface, nil } -func (r *reconcile) createTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) error { +func (r *reconcileNodeNetworkConfig) createTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) error { for i := range intended { alreadyExists := false for _, cfg := range existing { @@ -363,7 +375,7 @@ func (r *reconcile) createTaasNetlink(existing []nl.TaasInformation, intended [] return nil } -func (r *reconcile) reconcileExisting(cfg nl.VRFInformation) error { +func (r *reconcileNodeNetworkConfig) reconcileExisting(cfg nl.VRFInformation) error { if err := r.netlinkManager.EnsureBPFProgram(cfg); err != nil { return fmt.Errorf("error ensuring BPF program on VRF") } diff --git a/pkg/reconciler/nodenetworkconfig_reconciler.go b/pkg/reconciler/nodenetworkconfig_reconciler.go new file mode 100644 index 00000000..e03d3cea --- /dev/null +++ b/pkg/reconciler/nodenetworkconfig_reconciler.go @@ -0,0 +1,282 @@ +package reconciler + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "os" + "time" + + "github.com/go-logr/logr" + "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/anycast" + "github.com/telekom/das-schiff-network-operator/pkg/config" + "github.com/telekom/das-schiff-network-operator/pkg/frr" + "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" + "github.com/telekom/das-schiff-network-operator/pkg/nl" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + defaultDebounceTime = 1 * time.Second + + DefaultNodeNetworkConfigPath = "/opt/network-operator/current-config.yaml" + NodeNetworkConfigFilePerm = 0o600 +) + +type NodeNetworkConfigReconciler struct { + client client.Client + netlinkManager *nl.Manager + frrManager frr.ManagerInterface + anycastTracker *anycast.Tracker + config *config.Config + logger logr.Logger + healthChecker *healthcheck.HealthChecker + NodeNetworkConfig *v1alpha1.NodeNetworkConfig + NodeNetworkConfigPath string +} + +type reconcileNodeNetworkConfig struct { + *NodeNetworkConfigReconciler + logr.Logger +} + +func NewNodeNetworkConfigReconciler(clusterClient client.Client, anycastTracker *anycast.Tracker, logger logr.Logger, nodeNetworkConfigPath string, frrManager frr.ManagerInterface, netlinkManager *nl.Manager) (*NodeNetworkConfigReconciler, error) { + reconciler := &NodeNetworkConfigReconciler{ + client: clusterClient, + netlinkManager: netlinkManager, + frrManager: frrManager, + anycastTracker: anycastTracker, + logger: logger, + NodeNetworkConfigPath: nodeNetworkConfigPath, + } + + cfg, err := config.LoadConfig() + if err != nil { + return nil, fmt.Errorf("error loading config: %w", err) + } + reconciler.config = cfg + + if val := os.Getenv("FRR_CONFIG_FILE"); val != "" { + reconciler.frrManager.SetConfigPath(val) + } + + if err := reconciler.frrManager.Init(cfg.SkipVRFConfig[0]); err != nil { + return nil, fmt.Errorf("error trying to init FRR Manager: %w", err) + } + + nc, err := healthcheck.LoadConfig(healthcheck.NetHealthcheckFile) + if err != nil { + return nil, fmt.Errorf("error loading networking healthcheck config: %w", err) + } + + tcpDialer := healthcheck.NewTCPDialer(nc.Timeout) + reconciler.healthChecker, err = healthcheck.NewHealthChecker(reconciler.client, + healthcheck.NewDefaultHealthcheckToolkit(reconciler.frrManager, tcpDialer), + nc) + if err != nil { + return nil, fmt.Errorf("error creating networking healthchecker: %w", err) + } + + reconciler.NodeNetworkConfig, err = readNodeNetworkConfig(reconciler.NodeNetworkConfigPath) + if !errors.Is(err, os.ErrNotExist) { + return nil, fmt.Errorf("error reading NodeNetworkConfig from disk: %w", err) + } + + return reconciler, nil +} + +func (reconciler *NodeNetworkConfigReconciler) Reconcile(ctx context.Context) error { + r := &reconcileNodeNetworkConfig{ + NodeNetworkConfigReconciler: reconciler, + Logger: reconciler.logger, + } + + if err := r.config.ReloadConfig(); err != nil { + return fmt.Errorf("error reloading network-operator config: %w", err) + } + + // get NodeNetworkConfig from apiserver + cfg, err := r.fetchNodeConfig(ctx) + if err != nil { + // discard IsNotFound error + if apierrors.IsNotFound(err) { + return nil + } + return err + } + + if r.NodeNetworkConfig != nil && r.NodeNetworkConfig.Spec.Revision == cfg.Spec.Revision { + // replace in-memory working NodeNetworkConfig and store it on the disk + if err := reconciler.storeConfig(cfg, reconciler.NodeNetworkConfigPath); err != nil { + return fmt.Errorf("error saving NodeNetworkConfig status: %w", err) + } + + // current in-memory conifg has the same revision as the fetched one + // this means that NodeNetworkConfig was already provisioned - skip + if cfg.Status.ConfigStatus != StatusProvisioned { + if err := setStatus(ctx, r.client, cfg, StatusProvisioned, r.logger); err != nil { + return fmt.Errorf("error setting NodeNetworkConfig status: %w", err) + } + } + return nil + } + + // NodeNetworkConfig is invalid - discard + if cfg.Status.ConfigStatus == StatusInvalid { + r.logger.Info("skipping invalid NodeNetworkConfig", "name", cfg.Name) + return nil + } + if err := r.processConfig(ctx, cfg); err != nil { + return fmt.Errorf("error while processing NodeNetworkConfig: %w", err) + } + + // replace in-memory working NodeNetworkConfig and store it on the disk + if err := reconciler.storeConfig(cfg, reconciler.NodeNetworkConfigPath); err != nil { + return fmt.Errorf("error saving NodeNetworkConfig status: %w", err) + } + + return nil +} + +func (r *reconcileNodeNetworkConfig) processConfig(ctx context.Context, cfg *v1alpha1.NodeNetworkConfig) error { + // set NodeNetworkConfig status as provisioning + if err := setStatus(ctx, r.client, cfg, StatusProvisioning, r.logger); err != nil { + return fmt.Errorf("error setting NodeNetworkConfig status %s: %w", StatusProvisioning, err) + } + + // reconcile NodeNetworkConfig + if err := doReconciliation(r, cfg); err != nil { + // if reconciliation failed set NodeNetworkConfig's status as invalid and restore last known working NodeNetworkConfig + if err := r.invalidateAndRestore(ctx, cfg, "reconciliation failed"); err != nil { + return fmt.Errorf("reconciler restoring NodeNetworkConfig: %w", err) + } + + return fmt.Errorf("reconciler error: %w", err) + } + + // check if node is healthly after reconciliation + if err := r.checkHealth(ctx); err != nil { + // if node is not healthly set NodeNetworkConfig's status as invalid and restore last known working NodeNetworkConfig + if err := r.invalidateAndRestore(ctx, cfg, "healthcheck failed"); err != nil { + return fmt.Errorf("failed to restore NodeNetworkConfig: %w", err) + } + + return fmt.Errorf("healthcheck error (previous NodeNetworkConfig restored): %w", err) + } + + // set NodeNetworkConfig status as provisioned (valid) + if err := setStatus(ctx, r.client, cfg, StatusProvisioned, r.logger); err != nil { + return fmt.Errorf("error setting NodeNetworkConfig status %s: %w", StatusProvisioned, err) + } + + return nil +} + +func setStatus(ctx context.Context, c client.Client, cfg *v1alpha1.NodeNetworkConfig, status string, logger logr.Logger) error { + logger.Info("setting NodeNetworkConfig status", "name", cfg.Name, "status", status) + cfg.Status.ConfigStatus = status + cfg.Status.LastUpdate = metav1.Now() + if err := c.Status().Update(ctx, cfg); err != nil { + return fmt.Errorf("error updating NodeNetworkConfig status: %w", err) + } + return nil +} + +func (r *reconcileNodeNetworkConfig) invalidateAndRestore(ctx context.Context, cfg *v1alpha1.NodeNetworkConfig, reason string) error { + r.logger.Info("invalidating NodeNetworkConfig", "name", cfg.Name, "reason", reason) + if err := setStatus(ctx, r.client, cfg, StatusInvalid, r.logger); err != nil { + return fmt.Errorf("error invalidating NodeNetworkConfig: %w", err) + } + + // try to restore previously known good NodeNetworkConfig + r.logger.Info("restoring previous NodeNetworkConfig") + if err := r.restoreNodeNetworkConfig(); err != nil { + return fmt.Errorf("error restoring NodeNetworkConfig: %w", err) + } + + return nil +} + +func doReconciliation(r *reconcileNodeNetworkConfig, nodeCfg *v1alpha1.NodeNetworkConfig) error { + r.logger.Info("config to reconcile", "NodeNetworkConfig", *nodeCfg) + l3vnis := nodeCfg.Spec.Vrf + l2vnis := nodeCfg.Spec.Layer2 + taas := nodeCfg.Spec.RoutingTable + + if err := r.reconcileLayer3(l3vnis, taas); err != nil { + return err + } + if err := r.reconcileLayer2(l2vnis); err != nil { + return err + } + + return nil +} + +func (r *reconcileNodeNetworkConfig) restoreNodeNetworkConfig() error { + if r.NodeNetworkConfig == nil { + return nil + } + if err := doReconciliation(r, r.NodeNetworkConfig); err != nil { + return fmt.Errorf("error restoring NodeNetworkConfig: %w", err) + } + + r.logger.Info("restored last known valid NodeNetworkConfig") + + return nil +} + +func readNodeNetworkConfig(path string) (*v1alpha1.NodeNetworkConfig, error) { + cfg, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading NodeNetworkConfig: %w", err) + } + + nodeNetworkConfig := &v1alpha1.NodeNetworkConfig{} + if err := json.Unmarshal(cfg, nodeNetworkConfig); err != nil { + return nil, fmt.Errorf("error unmarshalling NodeNetworkConfig: %w", err) + } + + return nodeNetworkConfig, nil +} + +func (reconciler *NodeNetworkConfigReconciler) storeConfig(cfg *v1alpha1.NodeNetworkConfig, path string) error { + reconciler.NodeNetworkConfig = cfg + // save working NodeNetworkConfig + c, err := json.MarshalIndent(*reconciler.NodeNetworkConfig, "", " ") + if err != nil { + panic(err) + } + + if err = os.WriteFile(path, c, NodeNetworkConfigFilePerm); err != nil { + return fmt.Errorf("error saving NodeNetworkConfig status: %w", err) + } + + return nil +} + +func (reconciler *NodeNetworkConfigReconciler) checkHealth(ctx context.Context) error { + _, err := reconciler.healthChecker.IsFRRActive() + if err != nil { + return fmt.Errorf("error checking FRR status: %w", err) + } + if err := reconciler.healthChecker.CheckInterfaces(); err != nil { + return fmt.Errorf("error checking network interfaces: %w", err) + } + if err := reconciler.healthChecker.CheckReachability(); err != nil { + return fmt.Errorf("error checking network reachability: %w", err) + } + if err := reconciler.healthChecker.CheckAPIServer(ctx); err != nil { + return fmt.Errorf("error checking API Server reachability: %w", err) + } + if !reconciler.healthChecker.TaintsRemoved() { + if err := reconciler.healthChecker.RemoveTaints(ctx); err != nil { + return fmt.Errorf("error removing taint from the node: %w", err) + } + } + return nil +} diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go deleted file mode 100644 index 2c430f04..00000000 --- a/pkg/reconciler/reconciler.go +++ /dev/null @@ -1,130 +0,0 @@ -package reconciler - -import ( - "context" - "fmt" - "os" - "time" - - "github.com/go-logr/logr" - "github.com/telekom/das-schiff-network-operator/pkg/anycast" - "github.com/telekom/das-schiff-network-operator/pkg/config" - "github.com/telekom/das-schiff-network-operator/pkg/debounce" - "github.com/telekom/das-schiff-network-operator/pkg/frr" - "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" - "github.com/telekom/das-schiff-network-operator/pkg/nl" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -const defaultDebounceTime = 20 * time.Second - -type Reconciler struct { - client client.Client - netlinkManager *nl.Manager - frrManager *frr.Manager - anycastTracker *anycast.Tracker - config *config.Config - logger logr.Logger - healthChecker *healthcheck.HealthChecker - - debouncer *debounce.Debouncer -} - -type reconcile struct { - *Reconciler - logr.Logger -} - -func NewReconciler(clusterClient client.Client, anycastTracker *anycast.Tracker, logger logr.Logger) (*Reconciler, error) { - reconciler := &Reconciler{ - client: clusterClient, - netlinkManager: nl.NewManager(&nl.Toolkit{}), - frrManager: frr.NewFRRManager(), - anycastTracker: anycastTracker, - logger: logger, - } - - reconciler.debouncer = debounce.NewDebouncer(reconciler.reconcileDebounced, defaultDebounceTime, logger) - - cfg, err := config.LoadConfig() - if err != nil { - return nil, fmt.Errorf("error loading config: %w", err) - } - reconciler.config = cfg - - if val := os.Getenv("FRR_CONFIG_FILE"); val != "" { - reconciler.frrManager.ConfigPath = val - } - if err := reconciler.frrManager.Init(cfg.SkipVRFConfig[0]); err != nil { - return nil, fmt.Errorf("error trying to init FRR Manager: %w", err) - } - - nc, err := healthcheck.LoadConfig(healthcheck.NetHealthcheckFile) - if err != nil { - return nil, fmt.Errorf("error loading networking healthcheck config: %w", err) - } - - tcpDialer := healthcheck.NewTCPDialer(nc.Timeout) - reconciler.healthChecker, err = healthcheck.NewHealthChecker(reconciler.client, - healthcheck.NewDefaultHealthcheckToolkit(reconciler.frrManager, tcpDialer), - nc) - if err != nil { - return nil, fmt.Errorf("error creating netwokring healthchecker: %w", err) - } - - return reconciler, nil -} - -func (reconciler *Reconciler) Reconcile(ctx context.Context) { - reconciler.debouncer.Debounce(ctx) -} - -func (reconciler *Reconciler) reconcileDebounced(ctx context.Context) error { - r := &reconcile{ - Reconciler: reconciler, - Logger: reconciler.logger, - } - - r.Logger.Info("Reloading config") - if err := r.config.ReloadConfig(); err != nil { - return fmt.Errorf("error reloading network-operator config: %w", err) - } - - l3vnis, err := r.fetchLayer3(ctx) - if err != nil { - return err - } - l2vnis, err := r.fetchLayer2(ctx) - if err != nil { - return err - } - taas, err := r.fetchTaas(ctx) - if err != nil { - return err - } - - if err := r.reconcileLayer3(l3vnis, taas); err != nil { - return err - } - if err := r.reconcileLayer2(l2vnis); err != nil { - return err - } - - if !reconciler.healthChecker.IsNetworkingHealthy() { - _, err := reconciler.healthChecker.IsFRRActive() - if err != nil { - return fmt.Errorf("error checking FRR status: %w", err) - } - if err = reconciler.healthChecker.CheckInterfaces(); err != nil { - return fmt.Errorf("error checking network interfaces: %w", err) - } - if err = reconciler.healthChecker.CheckReachability(); err != nil { - return fmt.Errorf("error checking network reachability: %w", err) - } - if err = reconciler.healthChecker.RemoveTaints(ctx); err != nil { - return fmt.Errorf("error removing taint from the node: %w", err) - } - } - - return nil -} diff --git a/pkg/reconciler/reconciler_test.go b/pkg/reconciler/reconciler_test.go new file mode 100644 index 00000000..79a9ed95 --- /dev/null +++ b/pkg/reconciler/reconciler_test.go @@ -0,0 +1,385 @@ +package reconciler + +import ( + "context" + "encoding/json" + "fmt" + "os" + "testing" + "time" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/config" + mock_frr "github.com/telekom/das-schiff-network-operator/pkg/frr/mock" + "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" + "github.com/telekom/das-schiff-network-operator/pkg/nl" + mock_nl "github.com/telekom/das-schiff-network-operator/pkg/nl/mock" + "github.com/vishvananda/netlink" + "go.uber.org/mock/gomock" + "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +var ( + fakeNCRJSON = `{ + "apiVersion": "v1", + "items": [ + { + "apiVersion": "network.schiff.telekom.de/v1alpha1", + "kind": "NetworkConfigRevision", + "metadata": { + "creationTimestamp": "2024-07-11T15:16:00Z", + "generation": 1, + "name": "19dad916c7", + "resourceVersion": "91836", + "uid": "797e11da-1d60-4263-b2ad-fe0a73d761b7" + }, + "spec": { + "config": { + "layer2": [ + { + "id": 1, + "mtu": 1500, + "nodeSelector": { + "matchLabels": { + "worker": "true" + } + }, + "vni": 1 + } + ], + "revision": "", + "routingTable": [], + "vrf": [] + }, + "revision": "19dad916c701bc0aeebd14f66bae591f402cabd31cd9b150b87bca710abe3b33" + }, + "status": { + "isInvalid": false + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "" + } + }` + + fakeNodesJSON = `{"items":[ + { + "apiVersion": "v1", + "kind": "Node", + "metadata": { + "name": "kind-worker" + }, + "status": { + "conditions": [ + { + "status": "True", + "type": "Ready" + } + ] + } + } + ]}` + + fakeNNCJSON = ` + { + "apiVersion": "v1", + "items": [ + { + "apiVersion": "network.schiff.telekom.de/v1alpha1", + "kind": "NodeNetworkConfig", + "metadata": { + "creationTimestamp": "2024-07-11T15:14:32Z", + "generation": 4, + "name": "test-node", + "ownerReferences": [ + { + "apiVersion": "v1", + "kind": "Node", + "name": "test-node", + "uid": "a616532b-e188-41d7-a0f3-6f17cdfa50b8" + } + ], + "resourceVersion": "97276", + "uid": "b80f17a1-d68e-4e6d-b0cb-e2fdc97b0363" + }, + "spec": { + "layer2": [ + { + "id": 1, + "mtu": 1500, + "vni": 1 + } + ], + "revision": "19dad916c701bc0aeebd14f66bae591f402cabd31cd9b150b87bca710abe3b33", + "routingTable": [], + "vrf": [] + }, + "status": { + "configStatus": "provisioned" + } + } + ], + "kind": "List", + "metadata": { + "resourceVersion": "" + } + } +` + + mockctrl *gomock.Controller + tmpDir string + testConfig string +) + +const ( + operatorConfigEnv = "OPERATOR_CONFIG" + dummy = "dummy" +) + +var _ = BeforeSuite(func() { + var err error + tmpDir, err = os.MkdirTemp(".", "testdata") + Expect(err).ToNot(HaveOccurred()) + testConfig = tmpDir + "/config.yaml" + + config := config.Config{ + SkipVRFConfig: []string{dummy}, + } + + configData, err := yaml.Marshal(config) + Expect(err).ToNot(HaveOccurred()) + + err = os.WriteFile(testConfig, configData, 0o600) + Expect(err).ToNot(HaveOccurred()) + err = os.Setenv(operatorConfigEnv, testConfig) + Expect(err).ToNot(HaveOccurred()) + err = os.Setenv(healthcheck.NodenameEnv, "test-node") + Expect(err).ToNot(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + err := os.RemoveAll(tmpDir) + Expect(err).ToNot(HaveOccurred()) + err = os.Unsetenv(operatorConfigEnv) + Expect(err).ToNot(HaveOccurred()) + err = os.Unsetenv(healthcheck.NodenameEnv) + Expect(err).ToNot(HaveOccurred()) +}) + +func TestReconciler(t *testing.T) { + RegisterFailHandler(Fail) + mockctrl = gomock.NewController(t) + defer mockctrl.Finish() + RunSpecs(t, + "Reconciler Suite") +} + +var _ = Describe("ConfigReconciler", func() { + Context("NewConfigReconciler() should", func() { + It("return new config reconciler", func() { + c := createFullClient() + r, err := NewConfigReconciler(c, logr.New(nil), time.Millisecond*100) + Expect(r).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("ReconcileDebounced() should", func() { + It("return no error", func() { + c := createFullClient() + r, err := NewConfigReconciler(c, logr.New(nil), time.Millisecond*100) + Expect(r).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + err = r.ReconcileDebounced(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + }) +}) + +var _ = Describe("NodeConfigReconciler", func() { + Context("NewNodeConfigReconciler() should", func() { + It("return new node config reconciler", func() { + c := createClient() + r, err := NewNodeConfigReconciler(c, logr.New(nil), time.Millisecond*100, time.Millisecond*100, time.Millisecond*100, runtime.NewScheme(), 1) + Expect(r).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("reconcileDebaunced() should", func() { + It("return no error if there is nothing to deploy", func() { + c := createClient() + r, err := NewNodeConfigReconciler(c, logr.New(nil), time.Millisecond*100, time.Millisecond*100, time.Millisecond*100, runtime.NewScheme(), 1) + Expect(r).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + err = r.reconcileDebounced(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + It("return error if cannot set revision isInvalid status to false", func() { + fakeNCR := &v1alpha1.NetworkConfigRevisionList{} + err := json.Unmarshal([]byte(fakeNCRJSON), fakeNCR) + Expect(err).ShouldNot(HaveOccurred()) + c := createClient(fakeNCR) + r, err := NewNodeConfigReconciler(c, logr.New(nil), time.Millisecond*100, time.Millisecond*100, time.Millisecond*100, runtime.NewScheme(), 1) + Expect(r).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + err = r.reconcileDebounced(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + It("no error if NodeConfigRevision deployed successfully", func() { + c := createFullClient() + r, err := NewNodeConfigReconciler(c, logr.New(nil), time.Millisecond*100, time.Millisecond*100, time.Millisecond*100, runtime.NewScheme(), 1) + Expect(r).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + err = r.reconcileDebounced(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + It("return error on context timeout", func() { + fakeNCR := &v1alpha1.NetworkConfigRevisionList{} + err := json.Unmarshal([]byte(fakeNCRJSON), fakeNCR) + Expect(err).ShouldNot(HaveOccurred()) + fakeNodes := &corev1.NodeList{} + err = json.Unmarshal([]byte(fakeNodesJSON), fakeNodes) + Expect(err).ToNot(HaveOccurred()) + fakeNNC := &v1alpha1.NodeNetworkConfigList{} + err = json.Unmarshal([]byte(fakeNNCJSON), fakeNNC) + Expect(err).ShouldNot(HaveOccurred()) + + c := createClientWithStatus(&fakeNCR.Items[0], &fakeNNC.Items[0], fakeNCR, fakeNNC, fakeNodes) + r, err := NewNodeConfigReconciler(c, logr.New(nil), time.Millisecond*100, time.Millisecond*100, time.Millisecond*100, runtime.NewScheme(), 1) + Expect(r).ToNot(BeNil()) + Expect(err).ToNot(HaveOccurred()) + err = r.reconcileDebounced(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + }) +}) + +var _ = Describe("NodeNetworkConfigReconciler", func() { + Context("NewNodeNetworkConfigReconciler() should", func() { + It("return error if cannot init FRR Manager", func() { + frrManagerMock := mock_frr.NewMockManagerInterface(mockctrl) + c := createClient() + frrManagerMock.EXPECT().Init(gomock.Any()).Return(fmt.Errorf("init error")) + r, err := NewNodeNetworkConfigReconciler(c, nil, logr.New(nil), "", + frrManagerMock, nl.NewManager(mock_nl.NewMockToolkitInterface(mockctrl))) + Expect(err).To(HaveOccurred()) + Expect(r).To(BeNil()) + }) + It("create new reconciler", func() { + frrManagerMock := mock_frr.NewMockManagerInterface(mockctrl) + c := createClient() + frrManagerMock.EXPECT().Init(gomock.Any()).Return(nil) + r, err := NewNodeNetworkConfigReconciler(c, nil, logr.New(nil), "", + frrManagerMock, nl.NewManager(mock_nl.NewMockToolkitInterface(mockctrl))) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + }) + }) + Context("Reconcile() should", func() { + It("return no error if there is no config to reconcile", func() { + frrManagerMock := mock_frr.NewMockManagerInterface(mockctrl) + c := createClient() + frrManagerMock.EXPECT().Init(gomock.Any()).Return(nil) + r, err := NewNodeNetworkConfigReconciler(c, nil, logr.New(nil), "", + frrManagerMock, nl.NewManager(mock_nl.NewMockToolkitInterface(mockctrl))) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + It("return no error if there is no config to reconcile", func() { + frrManagerMock := mock_frr.NewMockManagerInterface(mockctrl) + c := createClient() + frrManagerMock.EXPECT().Init(gomock.Any()).Return(nil) + r, err := NewNodeNetworkConfigReconciler(c, nil, logr.New(nil), "", + frrManagerMock, nl.NewManager(mock_nl.NewMockToolkitInterface(mockctrl))) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + It("return error if cannot configure FRR", func() { + frrManagerMock := mock_frr.NewMockManagerInterface(mockctrl) + c := createFullClient() + frrManagerMock.EXPECT().Init(gomock.Any()).Return(nil) + frrManagerMock.EXPECT().Configure(gomock.Any(), + gomock.Any(), gomock.Any()).Return(false, fmt.Errorf("configuration error")) + r, err := NewNodeNetworkConfigReconciler(c, nil, logr.New(nil), "", + frrManagerMock, nl.NewManager(nil)) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + It("return error if failed to reload FRR", func() { + frrManagerMock := mock_frr.NewMockManagerInterface(mockctrl) + c := createFullClient() + frrManagerMock.EXPECT().Init(gomock.Any()).Return(nil) + frrManagerMock.EXPECT().Configure(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + frrManagerMock.EXPECT().ReloadFRR().Return(fmt.Errorf("error reloading FRR")) + frrManagerMock.EXPECT().RestartFRR().Return(fmt.Errorf("error restarting FRR")) + r, err := NewNodeNetworkConfigReconciler(c, nil, logr.New(nil), "", + frrManagerMock, nl.NewManager(nil)) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + It("return error if cannot configure networking", func() { + frrManagerMock := mock_frr.NewMockManagerInterface(mockctrl) + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{}, nil).Times(3) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(fmt.Errorf("link add error")) + + c := createFullClient() + frrManagerMock.EXPECT().Init(gomock.Any()).Return(nil) + frrManagerMock.EXPECT().Configure(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + frrManagerMock.EXPECT().ReloadFRR().Return(nil) + r, err := NewNodeNetworkConfigReconciler(c, nil, logr.New(nil), "", + frrManagerMock, nl.NewManager(netlinkMock)) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + }) +}) + +func createClient(initObjs ...runtime.Object) client.Client { + cb := clientBuilder(initObjs...) + return cb.Build() +} + +func createClientWithStatus(ncr, nnc client.Object, initObjs ...runtime.Object) client.Client { + return clientBuilder(initObjs...).WithStatusSubresource(nnc, ncr).Build() +} + +func clientBuilder(initObjs ...runtime.Object) *fake.ClientBuilder { + s := runtime.NewScheme() + err := corev1.AddToScheme(s) + Expect(err).ToNot(HaveOccurred()) + err = v1alpha1.AddToScheme(s) + Expect(err).ToNot(HaveOccurred()) + return fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(initObjs...) +} + +func createFullClient() client.Client { + fakeNNC := &v1alpha1.NodeNetworkConfigList{} + err := json.Unmarshal([]byte(fakeNNCJSON), fakeNNC) + Expect(err).ShouldNot(HaveOccurred()) + + fakeNCR := &v1alpha1.NetworkConfigRevisionList{} + err = json.Unmarshal([]byte(fakeNCRJSON), fakeNCR) + Expect(err).ShouldNot(HaveOccurred()) + c := clientBuilder(fakeNNC, fakeNCR).WithStatusSubresource(&fakeNNC.Items[0], &fakeNCR.Items[0]).Build() + + return c +}