diff --git a/.golangci.yml b/.golangci.yml index 4cffff70..9492105a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -138,6 +138,9 @@ linters-settings: disabled: true funlen: lines: 65 + gosec: + excludes: + - G115 issues: exclude-rules: diff --git a/Dockerfile b/Dockerfile index a02d970d..f372dd0b 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 @@ -10,28 +10,19 @@ COPY go.sum go.sum # 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/manager/main.go main.go +COPY cmd/operator/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 manager main.go +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -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 623026d8..4682b871 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,12 @@ -# 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 +# Worker image URL to use all building/pushing image targets +WORKER_IMG ?= ghcr.io/telekom/worker:latest # ENVTEST_K8S_VERSION refers to the version of kubebuilder assets to be downloaded by envtest binary. ENVTEST_K8S_VERSION = 1.25 @@ -50,8 +54,13 @@ manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and Cust $(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases .PHONY: generate -generate: controller-gen bpf-generate ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. +generate: controller-gen bpf-generate ## Generate code containing DeepCopy, DeepCopyInto and DeepCopyObject method implementations and GRPC code. $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." + PATH=$(PATH):$(shell pwd)/bin $(PROTOC) --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pkg/worker/pb/worker.proto + +.PHONY: generate-protobuff +generate-protobuff: protoc ## Generate code containing DeepCopy, DeepCopyInto and DeepCopyObject method implementations. + PATH=$(PATH):$(shell pwd)/bin $(PROTOC) --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative pkg/worker/pb/worker.proto .PHONY: fmt fmt: ## Run go fmt against code. @@ -68,33 +77,72 @@ test: manifests generate fmt vet envtest ## Run tests. ##@ Build .PHONY: build -build: generate fmt vet ## Build manager binary. - go build -o bin/manager cmd/manager/main.go +build: generate fmt vet ## Build agent binary. + go build -o bin/operator cmd/operator/main.go + go build -o bin/agent cmd/agent/main.go + go build -o bin/frr-exporter cmd/frr-exporter/main.go + +.PHONY: operator-build +operator-build: generate fmt vet ## Build agent binary. + go build -o bin/operator cmd/operator/main.go -.PHONY: sidecar-build +.PHONY: agent-build +agent-build: generate fmt vet ## Build agent binary. + go build -o bin/agent cmd/agent/main.go + +.PHONY: sidecar-build ## Build sidecar (frr-exporter) binary. sidecar-build: build go build -o bin/frr-exporter cmd/frr-exporter/main.go +.PHONY: worker-build ## Build worker binary. +agent-build: generate fmt vet + go build -o bin/worker cmd/worker/main.go + .PHONY: run run: manifests generate fmt vet ## Run a controller from your host. - go run ./cmd/manager/main.go + go run ./cmd/agent/main.go .PHONY: docker-build docker-build: test ## Build docker image with the manager. - docker build -t ${IMG} . + docker build -t ${OPERATOR_IMG} . + docker build -t ${AGENT_IMG} -f agent.Dockerfile . + docker build -t ${SIDECAR_IMG} -f frr-exporter.Dockerfile . + docker build -t ${WORKER_IMG} -f worker.Dockerfile . + +.PHONY: docker-build-agent +docker-build-agent: test ## Build docker image with the manager. + docker build -t ${AGENT_IMG} -f agent.Dockerfile . .PHONY: docker-build-sidecar docker-build-sidecar: test ## Build docker image with the manager. docker build -t ${SIDECAR_IMG} -f frr-exporter.Dockerfile . +.PHONY: docker-build-operator +docker-build-operator: test ## Build docker image with the manager. + docker build -t ${OPERATOR_IMG} . + +.PHONY: docker-build-worker +docker-build-worker: test ## Build docker image with the manager. + docker build -t ${WORKER_IMG} -f worker.Dockerfile . + .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} + +.PHONY: docker-push-worker +docker-push-worker: ## Push docker image with the manager. + docker push ${WORKER_IMG} ##@ Release @@ -133,8 +181,10 @@ 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} + cd config/worker && $(KUSTOMIZE) edit set image worker=${WORKER_IMG} $(KUSTOMIZE) build config/default | kubectl apply -f - .PHONY: undeploy @@ -174,3 +224,10 @@ GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ rm -rf $$TMP_DIR ;\ } endef + +PROTOC_DIR = $(shell pwd)/bin/protoc +PROTOC = $(shell pwd)/bin/protoc/bin/protoc +.PHONY: protoc +protoc: ## Download controller-gen locally if necessary. + mkdir -p $(PROTOC_DIR) && cd $(PROTOC_DIR) && wget -nc https://github.com/protocolbuffers/protobuf/releases/download/v27.0/protoc-27.0-linux-x86_64.zip 2> /dev/null && unzip -nqq protoc-27.0-linux-x86_64.zip + $(call go-get-tool,$(PROTOC_DIR),google.golang.org/protobuf/cmd/protoc-gen-go@v1.33.0) 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 71% rename from cmd/manager/main.go rename to cmd/agent/main.go index 536390ab..4d312e80 100644 --- a/cmd/manager/main.go +++ b/cmd/agent/main.go @@ -31,6 +31,8 @@ import ( networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" "github.com/telekom/das-schiff-network-operator/controllers" + adnetns "github.com/telekom/das-schiff-network-operator/pkg/adapters/netns" + advrfigbp "github.com/telekom/das-schiff-network-operator/pkg/adapters/vrf_igbp" "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" @@ -41,6 +43,7 @@ import ( "github.com/telekom/das-schiff-network-operator/pkg/nl" "github.com/telekom/das-schiff-network-operator/pkg/notrack" "github.com/telekom/das-schiff-network-operator/pkg/reconciler" + "github.com/telekom/das-schiff-network-operator/pkg/worker" // 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. @@ -88,46 +91,52 @@ func initCollectors() error { return nil } -func main() { - var onlyBPFMode bool - var configFile string - var interfacePrefix string - flag.StringVar(&configFile, "config", "", +type flags struct { + onlyBPFMode bool + configFile string + interfacePrefix string + nodeNetworkConfigPath string + workerType string + workerPort int + workerAddr string +} + +func getFlags() *flags { + f := flags{} + flag.StringVar(&f.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.BoolVar(&onlyBPFMode, "only-attach-bpf", false, + flag.BoolVar(&f.onlyBPFMode, "only-attach-bpf", false, "Only attach BPF to specified interfaces in config. This will not start any reconciliation. Perfect for masters.") - flag.StringVar(&interfacePrefix, "macvlan-interface-prefix", "", + flag.StringVar(&f.interfacePrefix, "macvlan-interface-prefix", "", "Interface prefix for bridge devices for MACVlan sync") + flag.StringVar(&f.nodeNetworkConfigPath, "nodenetworkconfig-path", reconciler.DefaultNodeNetworkConfigPath, + "Path to store working node configuration.") + flag.StringVar(&f.workerType, "agent", "vrf-igbp", "Use selected worker type (default: vrf-igbp).") + flag.StringVar(&f.workerAddr, "agentAddr", "", "Worker's address (default: '').") + flag.IntVar(&f.workerPort, "agentPort", worker.DefaultPort, fmt.Sprintf("Worker's port (default: %d).", worker.DefaultPort)) + flag.Parse() + return &f +} + +func main() { + f := getFlags() opts := zap.Options{ Development: true, } + opts.BindFlags(flag.CommandLine) - 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(f.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) @@ -146,14 +155,26 @@ func main() { os.Exit(1) } - if err := initComponents(mgr, anycastTracker, cfg, clientConfig, onlyBPFMode); err != nil { + var workerClient worker.Client + if f.workerType == "vrf-igbp" { + workerClient, err = advrfigbp.NewClient(fmt.Sprintf("%s:%d", f.workerAddr, f.workerPort)) + } else { + workerClient, err = adnetns.NewClient() + } + + if err != nil { + setupLog.Error(err, "failed to create adapter") + os.Exit(1) + } + + if err := initComponents(mgr, anycastTracker, cfg, clientConfig, f.onlyBPFMode, f.nodeNetworkConfigPath, workerClient); err != nil { setupLog.Error(err, "unable to initialize components") os.Exit(1) } - if interfacePrefix != "" { + if f.interfacePrefix != "" { setupLog.Info("start macvlan sync") - macvlan.RunMACSync(interfacePrefix) + macvlan.RunMACSync(f.interfacePrefix) } setupLog.Info("starting manager") @@ -163,10 +184,32 @@ 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, workerClient worker.Client) error { // Start VRFRouteConfigurationReconciler when we are not running in only BPF mode. if !onlyBPFMode { - if err := setupReconcilers(mgr, anycastTracker); err != nil { + if err := setupReconcilers(mgr, nodeConfigPath, workerClient); err != nil { return fmt.Errorf("unable to setup reconcilers: %w", err) } } @@ -225,34 +268,31 @@ func initComponents(mgr manager.Manager, anycastTracker *anycast.Tracker, cfg *c return nil } -func setupReconcilers(mgr manager.Manager, anycastTracker *anycast.Tracker) error { - r, err := reconciler.NewReconciler(mgr.GetClient(), anycastTracker, mgr.GetLogger()) +func setupReconcilers(mgr manager.Manager, nodeConfigPath string, workerClient worker.Client) error { + nc, err := healthcheck.LoadConfig(healthcheck.NetHealthcheckFile) if err != nil { - return fmt.Errorf("unable to create debounced reconciler: %w", err) + return fmt.Errorf("error loading networking healthcheck config: %w", err) } - 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) + tcpDialer := healthcheck.NewTCPDialer(nc.Timeout) + healthchecker, err := healthcheck.NewHealthChecker(mgr.GetClient(), + healthcheck.NewDefaultHealthcheckToolkit(nil, tcpDialer), nc) + if err != nil { + return fmt.Errorf("error creating networking healthchecker: %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) + r, err := reconciler.NewNodeNetworkConfigReconciler(mgr.GetClient(), mgr.GetLogger(), + nodeConfigPath, workerClient, healthchecker) + if err != nil { + return 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 fmt.Errorf("unable to create NodeConfig controller: %w", err) } return nil diff --git a/cmd/operator/main.go b/cmd/operator/main.go new file mode 100644 index 00000000..45f636aa --- /dev/null +++ b/cmd/operator/main.go @@ -0,0 +1,199 @@ +/* +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" + + // 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() { + 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/cmd/worker/main.go b/cmd/worker/main.go new file mode 100644 index 00000000..433c90be --- /dev/null +++ b/cmd/worker/main.go @@ -0,0 +1,70 @@ +package main + +import ( + "flag" + "fmt" + "net" + "os" + + "github.com/go-logr/zapr" + vrfigbpadapter "github.com/telekom/das-schiff-network-operator/pkg/adapters/vrf_igbp" + "github.com/telekom/das-schiff-network-operator/pkg/anycast" + "github.com/telekom/das-schiff-network-operator/pkg/frr" + "github.com/telekom/das-schiff-network-operator/pkg/nl" + "github.com/telekom/das-schiff-network-operator/pkg/worker" + workerpb "github.com/telekom/das-schiff-network-operator/pkg/worker/pb" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +func main() { + var agentType string + var port int + flag.StringVar(&agentType, "agent", "vrf-igbp", "Use selected agent type (default: vrf-igbp).") + flag.IntVar(&port, "port", worker.DefaultPort, fmt.Sprintf("gRPC listening port. (default: %d)", worker.DefaultPort)) + + zc := zap.NewProductionConfig() + zc.Level = zap.NewAtomicLevelAt(zap.DebugLevel) + zc.DisableStacktrace = true + z, _ := zc.Build() + log := zapr.NewLogger(z) + log = log.WithName("agent") + + log.Info("agent's port", "port", port) + + anycastTracker := anycast.NewTracker(&nl.Toolkit{}) + + var err error + var adapter worker.Adapter + switch agentType { + case "vrf-igbp": + adapter, err = vrfigbpadapter.New(anycastTracker, log, frr.NewFRRManager(), nl.NewManager(&nl.Toolkit{})) + default: + log.Error(fmt.Errorf("agent is currently not supported"), "type", agentType) + os.Exit(1) + } + + if err != nil { + log.Error(err, "error creating adapter") + os.Exit(1) + } + + log.Info("created adapter", "type", agentType) + + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) + if err != nil { + log.Error(err, "error on listening start") + os.Exit(1) + } + + grpcServer := grpc.NewServer([]grpc.ServerOption{}...) + srv := worker.NewServer(adapter, &log) + workerpb.RegisterAgentServer(grpcServer, srv) + + log.Info("created server, start listening...") + + if err := grpcServer.Serve(lis); err != nil { + log.Error(err, "grpc server error") + os.Exit(1) + } +} diff --git a/config/manager/manager.yaml b/config/agent/agent.yaml similarity index 85% rename from config/manager/manager.yaml rename to config/agent/agent.yaml index 16497def..3bc82ea1 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 @@ -74,10 +74,6 @@ spec: - mountPath: /opt/network-operator/config.yaml name: network-config subPath: config.yaml - - mountPath: /etc/frr - name: frr-config - - mountPath: /var/run/dbus/system_bus_socket - name: dbus-socket - command: - /frr-exporter env: @@ -119,10 +115,6 @@ spec: hostPath: path: /var/run/frr type: Directory - - name: dbus-socket - hostPath: - path: /var/run/dbus/system_bus_socket - type: Socket - configMap: name: network-operator-config name: network-config 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..5050527a 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -42,7 +42,9 @@ kind: Kustomization resources: - ../crd - ../rbac -- ../manager +- ../agent +- ../operator +- ../worker - ../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/config/worker/kustomization.yaml b/config/worker/kustomization.yaml new file mode 100644 index 00000000..bd5e8dd4 --- /dev/null +++ b/config/worker/kustomization.yaml @@ -0,0 +1,12 @@ +resources: +- worker.yaml + +generatorOptions: + disableNameSuffixHash: true + +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: worker + newName: ghcr.io/telekom/worker + newTag: latest diff --git a/config/worker/worker.yaml b/config/worker/worker.yaml new file mode 100644 index 00000000..cc2181ec --- /dev/null +++ b/config/worker/worker.yaml @@ -0,0 +1,82 @@ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: worker + namespace: system + labels: + app.kubernetes.io/component: worker +spec: + selector: + matchLabels: + app.kubernetes.io/component: worker + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: worker + labels: + app.kubernetes.io/component: worker + 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: + - /worker + args: [] + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + image: worker:latest + name: worker + securityContext: + privileged: true + runAsUser: 0 + # TODO(user): Configure the resources accordingly based on the project requirements. + # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + resources: + limits: + cpu: 500m + memory: 128Mi + requests: + cpu: 10m + memory: 64Mi + volumeMounts: + - mountPath: /opt/network-operator/config.yaml + name: network-config + subPath: config.yaml + - mountPath: /etc/frr + name: frr-config + - mountPath: /var/run/dbus/system_bus_socket + name: dbus-socket + serviceAccountName: controller-manager + terminationGracePeriodSeconds: 10 + volumes: + - name: frr-config + hostPath: + path: /etc/frr + type: Directory + - name: dbus-socket + hostPath: + path: /var/run/dbus/system_bus_socket + type: Socket + - configMap: + name: network-operator-config + name: network-config 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 1d4c9be0..bf8a20dd 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..c38c64b2 100644 --- a/go.mod +++ b/go.mod @@ -5,43 +5,47 @@ 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/go-logr/zapr v1.2.4 + 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 + go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb - golang.org/x/sys v0.11.0 + golang.org/x/sys v0.24.0 + google.golang.org/grpc v1.67.1 + google.golang.org/protobuf v1.34.2 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/cespare/xxhash/v2 v2.3.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/go-logr/zapr v1.2.4 // indirect + github.com/fsnotify/fsnotify v1.7.0 // 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/uuid v1.3.0 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -53,29 +57,27 @@ 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/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 + go.uber.org/atomic v1.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.3.0 // 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/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // 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..8028c40a 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.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,40 +44,34 @@ 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= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= @@ -96,7 +83,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 +106,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 +139,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,55 +149,46 @@ 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= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 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 +203,48 @@ 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.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.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.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= 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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 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/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= +google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= 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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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 +259,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/adapters/netns/netns.go b/pkg/adapters/netns/netns.go new file mode 100644 index 00000000..12bd901d --- /dev/null +++ b/pkg/adapters/netns/netns.go @@ -0,0 +1,42 @@ +package adapters + +import ( + "context" + "errors" + + "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/worker" +) + +type netNS struct{} + +func New() (worker.Adapter, error) { + return &netNS{}, nil +} + +func (*netNS) CheckHealth() error { + return errors.ErrUnsupported +} + +func (*netNS) GetConfig() *config.Config { + return nil +} + +func (*netNS) ReconcileLayer3([]v1alpha1.VRFRouteConfigurationSpec, []v1alpha1.RoutingTableSpec) error { + return errors.ErrUnsupported +} + +func (*netNS) ReconcileLayer2([]v1alpha1.Layer2NetworkConfigurationSpec) error { + return errors.ErrUnsupported +} + +type netNSClient struct{} + +func NewClient() (worker.Client, error) { + return &netNSClient{}, errors.ErrUnsupported +} + +func (*netNSClient) SendConfig(context.Context, *v1alpha1.NodeNetworkConfig) error { + return errors.ErrUnsupported +} diff --git a/pkg/adapters/vrf_igbp/layer2.go b/pkg/adapters/vrf_igbp/layer2.go new file mode 100644 index 00000000..7940efb1 --- /dev/null +++ b/pkg/adapters/vrf_igbp/layer2.go @@ -0,0 +1,162 @@ +package adapters + +import ( + "fmt" + "net" + + networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/nl" +) + +func (r *VrfIgbp) ReconcileLayer2(l2vnis []networkv1alpha1.Layer2NetworkConfigurationSpec) error { + desired, err := r.getDesired(l2vnis) + if err != nil { + return err + } + + existing, err := r.netlinkManager.ListL2() + if err != nil { + return fmt.Errorf("error listing L2: %w", err) + } + + toDelete := determineToBeDeleted(existing, desired) + + create := []nl.Layer2Information{} + anycastTrackerInterfaces := []int{} + for i := range desired { + alreadyExists := false + var currentConfig nl.Layer2Information + for j := range existing { + if desired[i].VlanID == existing[j].VlanID { + alreadyExists = true + currentConfig = existing[j] + break + } + } + if !alreadyExists { + create = append(create, desired[i]) + } else { + if err := r.reconcileExistingLayer(&desired[i], ¤tConfig, &anycastTrackerInterfaces); err != nil { + return err + } + } + } + + for i := range toDelete { + r.logger.Info("Deleting Layer2 because it is no longer configured", "vlan", toDelete[i].VlanID, "vni", toDelete[i].VNI) + errs := r.netlinkManager.CleanupL2(&toDelete[i]) + for _, err := range errs { + r.logger.Error(err, "Error deleting Layer2", "vlan", toDelete[i].VlanID, "vni", toDelete[i].VNI) + } + } + + for i := range create { + if err := r.createL2(&create[i], &anycastTrackerInterfaces); err != nil { + return err + } + } + + r.anycastTracker.TrackedBridges = anycastTrackerInterfaces + + return nil +} + +func (r *VrfIgbp) 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 { + return fmt.Errorf("error creating layer2 vlan %d vni %d: %w", info.VlanID, info.VNI, err) + } + if info.AdvertiseNeighbors { + bridgeID, err := r.netlinkManager.GetBridgeID(info) + if err != nil { + return fmt.Errorf("error getting bridge id for vlanId %d: %w", info.VlanID, err) + } + *anycastTrackerInterfaces = append(*anycastTrackerInterfaces, bridgeID) + } + return nil +} + +func (r *VrfIgbp) 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) + } + + desired := []nl.Layer2Information{} + for i := range l2vnis { + spec := l2vnis[i] + + var anycastMAC *net.HardwareAddr + if mac, err := net.ParseMAC(spec.AnycastMac); err == nil { + anycastMAC = &mac + } + + anycastGateways, err := r.netlinkManager.ParseIPAddresses(spec.AnycastGateways) + if err != nil { + r.logger.Error(err, "error parsing anycast gateways", "gw", spec.AnycastGateways) + return nil, fmt.Errorf("error parsing anycast gateways: %w", err) + } + + if spec.VRF != "" { + vrfAvailable := false + for _, info := range availableVrfs { + if info.Name == spec.VRF { + vrfAvailable = true + break + } + } + if !vrfAvailable { + r.logger.Error(err, "VRF of Layer2 not found on node", "vrf", spec.VRF) + continue + } + } + + desired = append(desired, nl.Layer2Information{ + VlanID: spec.ID, + MTU: spec.MTU, + VNI: spec.VNI, + VRF: spec.VRF, + AnycastMAC: anycastMAC, + AnycastGateways: anycastGateways, + AdvertiseNeighbors: spec.AdvertiseNeighbors, + NeighSuppression: spec.NeighSuppression, + CreateMACVLANInterface: spec.CreateMACVLANInterface, + }) + } + + return desired, nil +} + +func determineToBeDeleted(existing, desired []nl.Layer2Information) []nl.Layer2Information { + toDelete := []nl.Layer2Information{} + for i := range existing { + stillExists := false + for j := range desired { + if desired[j].VlanID == existing[i].VlanID { + stillExists = true + break + } + } + if !stillExists { + toDelete = append(toDelete, existing[i]) + } + } + return toDelete +} + +func (r *VrfIgbp) 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 { + return fmt.Errorf("error reconciling layer2 vlan %d vni %d: %w", desired.VlanID, desired.VNI, err) + } + if desired.AdvertiseNeighbors { + bridgeID, err := r.netlinkManager.GetBridgeID(desired) + if err != nil { + return fmt.Errorf("error getting bridge id for vlanId %d: %w", desired.VlanID, err) + } + *anycastTrackerInterfaces = append(*anycastTrackerInterfaces, bridgeID) + } + return nil +} diff --git a/pkg/reconciler/layer3.go b/pkg/adapters/vrf_igbp/layer3.go similarity index 77% rename from pkg/reconciler/layer3.go rename to pkg/adapters/vrf_igbp/layer3.go index f66e91db..2c5c7c33 100644 --- a/pkg/reconciler/layer3.go +++ b/pkg/adapters/vrf_igbp/layer3.go @@ -1,7 +1,6 @@ -package reconciler +package adapters import ( - "context" "fmt" "net" "sort" @@ -16,30 +15,8 @@ import ( const defaultSleep = 2 * time.Second -func (r *reconcile) fetchLayer3(ctx context.Context) ([]networkv1alpha1.VRFRouteConfiguration, error) { - vrfs := &networkv1alpha1.VRFRouteConfigurationList{} - err := r.client.List(ctx, vrfs) - if err != nil { - r.Logger.Error(err, "error getting list of VRFs from Kubernetes") - return nil, fmt.Errorf("error getting list of VRFs from Kubernetes: %w", err) - } - - return vrfs.Items, nil -} - -func (r *reconcile) fetchTaas(ctx context.Context) ([]networkv1alpha1.RoutingTable, error) { - tables := &networkv1alpha1.RoutingTableList{} - err := r.client.List(ctx, tables) - if err != nil { - r.Logger.Error(err, "error getting list of TaaS from Kubernetes") - return nil, fmt.Errorf("error getting list of TaaS from Kubernetes: %w", err) - } - - return tables.Items, nil -} - // nolint: contextcheck // context is not relevant -func (r *reconcile) reconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfiguration, taas []networkv1alpha1.RoutingTable) error { +func (r *VrfIgbp) ReconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfigurationSpec, taas []networkv1alpha1.RoutingTableSpec) error { vrfConfigMap, err := r.createVrfConfigMap(l3vnis) if err != nil { return err @@ -67,7 +44,7 @@ func (r *reconcile) reconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfigurati created, deletedVRF, err := r.reconcileL3Netlink(l3Configs) if err != nil { - r.Logger.Error(err, "error reconciling Netlink") + r.logger.Error(err, "error reconciling Netlink") return err } @@ -89,20 +66,20 @@ func (r *reconcile) reconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfigurati time.Sleep(defaultSleep) for _, info := range created { if err := r.netlinkManager.UpL3(info); err != nil { - r.Logger.Error(err, "error setting L3 to state UP") + r.logger.Error(err, "error setting L3 to state UP") return fmt.Errorf("error setting L3 to state UP: %w", err) } } return nil } -func (r *reconcile) configureFRR(vrfConfigs []frr.VRFConfiguration, reloadTwice bool) error { +func (r *VrfIgbp) configureFRR(vrfConfigs []frr.VRFConfiguration, reloadTwice bool) error { changed, err := r.frrManager.Configure(frr.Configuration{ VRFs: vrfConfigs, ASN: r.config.ServerASN, }, r.netlinkManager, r.config) if err != nil { - r.Logger.Error(err, "error updating FRR configuration") + r.logger.Error(err, "error updating FRR configuration") return fmt.Errorf("error updating FRR configuration: %w", err) } @@ -127,27 +104,27 @@ func (r *reconcile) configureFRR(vrfConfigs []frr.VRFConfiguration, reloadTwice return nil } -func (r *reconcile) reloadFRR() error { - r.Logger.Info("trying to reload FRR config because it changed") +func (r *VrfIgbp) reloadFRR() error { + r.logger.Info("trying to reload FRR config because it changed") err := r.frrManager.ReloadFRR() if err != nil { - r.Logger.Error(err, "error reloading FRR systemd unit, trying restart") + r.logger.Error(err, "error reloading FRR systemd unit, trying restart") err = r.frrManager.RestartFRR() if err != nil { - r.Logger.Error(err, "error restarting FRR systemd unit") + r.logger.Error(err, "error restarting FRR systemd unit") return fmt.Errorf("error reloading / restarting FRR systemd unit: %w", err) } } - r.Logger.Info("reloaded FRR config") + r.logger.Info("reloaded FRR config") return nil } -func (r *reconcile) createVrfConfigMap(l3vnis []networkv1alpha1.VRFRouteConfiguration) (map[string]frr.VRFConfiguration, error) { +func (r *VrfIgbp) 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 @@ -163,13 +140,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, "name") continue } @@ -183,11 +160,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) @@ -242,7 +219,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 *VrfIgbp) 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) @@ -262,7 +239,7 @@ func (r *reconcile) reconcileL3Netlink(vrfConfigs []frr.VRFConfiguration) ([]nl. if !stillExists || existing[i].MarkForDelete { toDelete = append(toDelete, existing[i]) } else if err := r.reconcileExisting(existing[i]); err != nil { - r.Logger.Error(err, "error reconciling existing VRF", "vrf", existing[i].Name, "vni", strconv.Itoa(existing[i].VNI)) + r.logger.Error(err, "error reconciling existing VRF", "vrf", existing[i].Name, "vni", strconv.Itoa(existing[i].VNI)) } } @@ -271,15 +248,15 @@ func (r *reconcile) reconcileL3Netlink(vrfConfigs []frr.VRFConfiguration) ([]nl. // Delete / Cleanup VRFs for _, info := range toDelete { - r.Logger.Info("Deleting VRF because it is no longer configured in Kubernetes", "vrf", info.Name, "vni", info.VNI) + r.logger.Info("Deleting VRF because it is no longer configured in Kubernetes", "vrf", info.Name, "vni", info.VNI) errs := r.netlinkManager.CleanupL3(info.Name) for _, err := range errs { - r.Logger.Error(err, "Error deleting VRF", "vrf", info.Name, "vni", strconv.Itoa(info.VNI)) + r.logger.Error(err, "Error deleting VRF", "vrf", info.Name, "vni", strconv.Itoa(info.VNI)) } } // Create VRFs for _, info := range toCreate { - r.Logger.Info("Creating VRF to match Kubernetes", "vrf", info.Name, "vni", info.VNI) + r.logger.Info("Creating VRF to match Kubernetes", "vrf", info.Name, "vni", info.VNI) err := r.netlinkManager.CreateL3(info) if err != nil { return nil, false, fmt.Errorf("error creating VRF %s, VNI %d: %w", info.Name, info.VNI, err) @@ -289,7 +266,7 @@ func (r *reconcile) reconcileL3Netlink(vrfConfigs []frr.VRFConfiguration) ([]nl. return toCreate, len(toDelete) > 0, nil } -func (r *reconcile) reconcileTaasNetlink(vrfConfigs []frr.VRFConfiguration) (bool, error) { +func (r *VrfIgbp) 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) @@ -308,7 +285,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 *VrfIgbp) cleanupTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) (bool, error) { deletedInterface := false for _, cfg := range existing { stillExists := false @@ -328,7 +305,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 *VrfIgbp) createTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) error { for i := range intended { alreadyExists := false for _, cfg := range existing { @@ -351,7 +328,7 @@ func (r *reconcile) createTaasNetlink(existing []nl.TaasInformation, intended [] return nil } -func (r *reconcile) reconcileExisting(cfg nl.VRFInformation) error { +func (r *VrfIgbp) 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/adapters/vrf_igbp/vrfigbp.go b/pkg/adapters/vrf_igbp/vrfigbp.go new file mode 100644 index 00000000..ec287db9 --- /dev/null +++ b/pkg/adapters/vrf_igbp/vrfigbp.go @@ -0,0 +1,127 @@ +package adapters + +import ( + "context" + "encoding/json" + "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" + "github.com/telekom/das-schiff-network-operator/pkg/worker" + workerpb "github.com/telekom/das-schiff-network-operator/pkg/worker/pb" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +const defaultTimeout = 30 * time.Second + +type VrfIgbp struct { + netlinkManager *nl.Manager + config *config.Config + frrManager frr.ManagerInterface + anycastTracker *anycast.Tracker + dirtyFRRConfig bool + healthChecker healthcheck.Adapter + logger logr.Logger +} + +func New(anycastTracker *anycast.Tracker, logger logr.Logger, frrManager frr.ManagerInterface, netlinkManager *nl.Manager) (worker.Adapter, error) { + reconciler := &VrfIgbp{ + netlinkManager: netlinkManager, + frrManager: frrManager, + anycastTracker: anycastTracker, + logger: 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.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(nil, + healthcheck.NewDefaultHealthcheckToolkit(reconciler.frrManager, tcpDialer), + nc) + if err != nil { + return nil, fmt.Errorf("error creating networking healthchecker: %w", err) + } + + return reconciler, nil +} + +func (r *VrfIgbp) CheckHealth() error { + if _, err := r.healthChecker.IsFRRActive(); err != nil { + return fmt.Errorf("error checking FRR status: %w", err) + } + if err := r.healthChecker.CheckInterfaces(); err != nil { + return fmt.Errorf("error checking network interfaces: %w", err) + } + return nil +} + +func (r *VrfIgbp) GetConfig() *config.Config { + return r.config +} + +type vrfIgbpClient struct { + grpcClient workerpb.AgentClient +} + +func NewClient(address string) (worker.Client, error) { + var grpcOpts []grpc.DialOption + grpcOpts = append(grpcOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + conn, err := grpc.NewClient(address, grpcOpts...) + if err != nil { + return nil, fmt.Errorf("unable to create gRPC connection: %w", err) + } + + client := workerpb.NewAgentClient(conn) + + vrfigbpClient := vrfIgbpClient{ + grpcClient: client, + } + + return &vrfigbpClient, nil +} + +func (c *vrfIgbpClient) SendConfig(ctx context.Context, nodeConfig *v1alpha1.NodeNetworkConfig) error { + timeoutCtx, cancel := context.WithTimeout(ctx, defaultTimeout) + defer cancel() + + nc := workerpb.NetworkConfiguration{ + Data: []byte{}, + } + data, err := json.Marshal(*nodeConfig) + if err != nil { + return fmt.Errorf("error marshaling NodeConfig: %w", err) + } + + nc.Data = data + + if _, err = c.grpcClient.SetConfiguration(timeoutCtx, &nc); err != nil { + return fmt.Errorf("error setting configuration: %w", err) + } + + return nil +} 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..bd9be33e 100644 --- a/pkg/healthcheck/healthcheck.go +++ b/pkg/healthcheck/healthcheck.go @@ -41,14 +41,24 @@ var ( } ) +//go:generate mockgen -destination ./mock/mock_healthcheck.go . Adapter,FRRInterface,TCPDialerInterface +type Adapter interface { + CheckAPIServer(ctx context.Context) error + CheckInterfaces() error + CheckReachability() error + IsFRRActive() (bool, error) + RemoveTaints(ctx context.Context) error + TaintsRemoved() bool +} + // HealthChecker is a struct that holds data required for networking healthcheck. type HealthChecker struct { - client client.Client - isNetworkingHealthy bool - logr.Logger - netConfig *NetHealthcheckConfig - toolkit *Toolkit - retries int + client client.Client + taintsRemoved bool + logger logr.Logger + netConfig *NetHealthcheckConfig + toolkit *Toolkit + retries int } // NewHealthChecker creates new HealthChecker. @@ -61,18 +71,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 taintsRemoved bool. +func (hc *HealthChecker) TaintsRemoved() bool { + return hc.taintsRemoved } // RemoveTaints removes taint from the node. @@ -81,7 +91,7 @@ func (hc *HealthChecker) RemoveTaints(ctx context.Context) error { err := hc.client.Get(ctx, types.NamespacedName{Name: os.Getenv(NodenameEnv)}, node) if err != nil { - hc.Logger.Error(err, "error while getting node's info") + hc.logger.Error(err, "error while getting node's info") return fmt.Errorf("error while getting node's info: %w", err) } @@ -97,12 +107,12 @@ func (hc *HealthChecker) RemoveTaints(ctx context.Context) error { } if updateNode { if err := hc.client.Update(ctx, node, &client.UpdateOptions{}); err != nil { - hc.Logger.Error(err, "") + hc.logger.Error(err, "") return fmt.Errorf("error while updating node: %w", err) } } - hc.isNetworkingHealthy = true + hc.taintsRemoved = true return nil } @@ -126,7 +136,7 @@ func (hc *HealthChecker) CheckInterfaces() error { issuesFound := false for _, i := range hc.netConfig.Interfaces { if err := hc.checkInterface(i); err != nil { - hc.Logger.Error(err, "problem with network interface "+i) + hc.logger.Error(err, "problem with network interface "+i) issuesFound = true } } @@ -137,17 +147,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 +162,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) @@ -273,7 +291,6 @@ func NewDefaultHealthcheckToolkit(frr FRRInterface, tcpDialer TCPDialerInterface return NewHealthCheckToolkit(frr, netlink.LinkByName, tcpDialer) } -//go:generate mockgen -destination ./mock/mock_healthcheck.go . FRRInterface,TCPDialerInterface type FRRInterface interface { GetStatusFRR() (string, string, error) } 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/healthcheck/mock/mock_healthcheck.go b/pkg/healthcheck/mock/mock_healthcheck.go index 1e037615..ad6a578b 100644 --- a/pkg/healthcheck/mock/mock_healthcheck.go +++ b/pkg/healthcheck/mock/mock_healthcheck.go @@ -1,16 +1,125 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/telekom/das-schiff-network-operator/pkg/healthcheck (interfaces: FRRInterface,TCPDialerInterface) +// Source: github.com/telekom/das-schiff-network-operator/pkg/healthcheck (interfaces: Adapter,FRRInterface,TCPDialerInterface) // Package mock_healthcheck is a generated GoMock package. package mock_healthcheck import ( + context "context" net "net" reflect "reflect" gomock "go.uber.org/mock/gomock" ) +// MockAdapter is a mock of Adapter interface. +type MockAdapter struct { + ctrl *gomock.Controller + recorder *MockAdapterMockRecorder +} + +// MockAdapterMockRecorder is the mock recorder for MockAdapter. +type MockAdapterMockRecorder struct { + mock *MockAdapter +} + +// NewMockAdapter creates a new mock instance. +func NewMockAdapter(ctrl *gomock.Controller) *MockAdapter { + mock := &MockAdapter{ctrl: ctrl} + mock.recorder = &MockAdapterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAdapter) EXPECT() *MockAdapterMockRecorder { + return m.recorder +} + +// CheckAPIServer mocks base method. +func (m *MockAdapter) CheckAPIServer(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckAPIServer", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckAPIServer indicates an expected call of CheckAPIServer. +func (mr *MockAdapterMockRecorder) CheckAPIServer(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckAPIServer", reflect.TypeOf((*MockAdapter)(nil).CheckAPIServer), arg0) +} + +// CheckInterfaces mocks base method. +func (m *MockAdapter) CheckInterfaces() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckInterfaces") + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckInterfaces indicates an expected call of CheckInterfaces. +func (mr *MockAdapterMockRecorder) CheckInterfaces() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckInterfaces", reflect.TypeOf((*MockAdapter)(nil).CheckInterfaces)) +} + +// CheckReachability mocks base method. +func (m *MockAdapter) CheckReachability() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CheckReachability") + ret0, _ := ret[0].(error) + return ret0 +} + +// CheckReachability indicates an expected call of CheckReachability. +func (mr *MockAdapterMockRecorder) CheckReachability() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckReachability", reflect.TypeOf((*MockAdapter)(nil).CheckReachability)) +} + +// IsFRRActive mocks base method. +func (m *MockAdapter) IsFRRActive() (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsFRRActive") + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IsFRRActive indicates an expected call of IsFRRActive. +func (mr *MockAdapterMockRecorder) IsFRRActive() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsFRRActive", reflect.TypeOf((*MockAdapter)(nil).IsFRRActive)) +} + +// RemoveTaints mocks base method. +func (m *MockAdapter) RemoveTaints(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveTaints", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveTaints indicates an expected call of RemoveTaints. +func (mr *MockAdapterMockRecorder) RemoveTaints(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTaints", reflect.TypeOf((*MockAdapter)(nil).RemoveTaints), arg0) +} + +// TaintsRemoved mocks base method. +func (m *MockAdapter) TaintsRemoved() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TaintsRemoved") + ret0, _ := ret[0].(bool) + return ret0 +} + +// TaintsRemoved indicates an expected call of TaintsRemoved. +func (mr *MockAdapterMockRecorder) TaintsRemoved() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TaintsRemoved", reflect.TypeOf((*MockAdapter)(nil).TaintsRemoved)) +} + // MockFRRInterface is a mock of FRRInterface interface. type MockFRRInterface struct { ctrl *gomock.Controller 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..345693b4 --- /dev/null +++ b/pkg/reconciler/config_reconciler.go @@ -0,0 +1,250 @@ +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 (r *reconcileConfig) fetchLayer2(ctx context.Context) ([]v1alpha1.Layer2NetworkConfiguration, error) { + layer2List := &v1alpha1.Layer2NetworkConfigurationList{} + err := r.client.List(ctx, layer2List) + if err != nil { + r.Logger.Error(err, "error getting list of Layer2s from Kubernetes") + return nil, fmt.Errorf("error getting list of Layer2s from Kubernetes: %w", err) + } + + l2vnis := []v1alpha1.Layer2NetworkConfiguration{} + l2vnis = append(l2vnis, layer2List.Items...) + + if err := checkL2Duplicates(l2vnis); err != nil { + return nil, err + } + + return l2vnis, nil +} + +func (r *reconcileConfig) fetchLayer3(ctx context.Context) ([]v1alpha1.VRFRouteConfiguration, error) { + vrfs := &v1alpha1.VRFRouteConfigurationList{} + err := r.client.List(ctx, vrfs) + if err != nil { + r.Logger.Error(err, "error getting list of VRFs from Kubernetes") + return nil, fmt.Errorf("error getting list of VRFs from Kubernetes: %w", err) + } + + return vrfs.Items, nil +} + +func (r *reconcileConfig) fetchTaas(ctx context.Context) ([]v1alpha1.RoutingTable, error) { + tables := &v1alpha1.RoutingTableList{} + err := r.client.List(ctx, tables) + if err != nil { + r.Logger.Error(err, "error getting list of TaaS from Kubernetes") + return nil, fmt.Errorf("error getting list of TaaS from Kubernetes: %w", err) + } + + return tables.Items, 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 +} + +func checkL2Duplicates(configs []v1alpha1.Layer2NetworkConfiguration) error { + for i := range configs { + for j := i + 1; j < len(configs); j++ { + if configs[i].Spec.ID == configs[j].Spec.ID { + return fmt.Errorf("dupliate Layer2 ID found: %s %s", configs[i].ObjectMeta.Name, configs[j].ObjectMeta.Name) + } + if configs[i].Spec.VNI == configs[j].Spec.VNI { + return fmt.Errorf("dupliate Layer2 VNI found: %s %s", configs[i].ObjectMeta.Name, configs[j].ObjectMeta.Name) + } + } + } + return 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 deleted file mode 100644 index 941b28f6..00000000 --- a/pkg/reconciler/layer2.go +++ /dev/null @@ -1,244 +0,0 @@ -package reconciler - -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) { - layer2List := &networkv1alpha1.Layer2NetworkConfigurationList{} - err := r.client.List(ctx, layer2List) - if err != nil { - r.Logger.Error(err, "error getting list of Layer2s from Kubernetes") - 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) - } - - if err := r.checkL2Duplicates(l2vnis); err != nil { - return nil, err - } - - return l2vnis, nil -} - -func (r *reconcile) reconcileLayer2(l2vnis []networkv1alpha1.Layer2NetworkConfiguration) error { - desired, err := r.getDesired(l2vnis) - if err != nil { - return err - } - - existing, err := r.netlinkManager.ListL2() - if err != nil { - return fmt.Errorf("error listing L2: %w", err) - } - - toDelete := determineToBeDeleted(existing, desired) - - create := []nl.Layer2Information{} - anycastTrackerInterfaces := []int{} - for i := range desired { - alreadyExists := false - var currentConfig nl.Layer2Information - for j := range existing { - if desired[i].VlanID == existing[j].VlanID { - alreadyExists = true - currentConfig = existing[j] - break - } - } - if !alreadyExists { - create = append(create, desired[i]) - } else { - if err := r.reconcileExistingLayer(&desired[i], ¤tConfig, &anycastTrackerInterfaces); err != nil { - return err - } - } - } - - for i := range toDelete { - r.Logger.Info("Deleting Layer2 because it is no longer configured", "vlan", toDelete[i].VlanID, "vni", toDelete[i].VNI) - errs := r.netlinkManager.CleanupL2(&toDelete[i]) - for _, err := range errs { - r.Logger.Error(err, "Error deleting Layer2", "vlan", toDelete[i].VlanID, "vni", toDelete[i].VNI) - } - } - - for i := range create { - if err := r.createL2(&create[i], &anycastTrackerInterfaces); err != nil { - return err - } - } - - r.anycastTracker.TrackedBridges = anycastTrackerInterfaces - - return nil -} - -func (r *reconcile) 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 { - return fmt.Errorf("error creating layer2 vlan %d vni %d: %w", info.VlanID, info.VNI, err) - } - if info.AdvertiseNeighbors { - bridgeID, err := r.netlinkManager.GetBridgeID(info) - if err != nil { - return fmt.Errorf("error getting bridge id for vlanId %d: %w", info.VlanID, err) - } - *anycastTrackerInterfaces = append(*anycastTrackerInterfaces, bridgeID) - } - return nil -} - -func (r *reconcile) getDesired(l2vnis []networkv1alpha1.Layer2NetworkConfiguration) ([]nl.Layer2Information, error) { - availableVrfs, err := r.netlinkManager.ListL3() - if err != nil { - return nil, fmt.Errorf("error loading available VRFs: %w", err) - } - - desired := []nl.Layer2Information{} - for i := range l2vnis { - spec := l2vnis[i].Spec - - var anycastMAC *net.HardwareAddr - if mac, err := net.ParseMAC(spec.AnycastMac); err == nil { - anycastMAC = &mac - } - - 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) - return nil, fmt.Errorf("error parsing anycast gateways: %w", err) - } - - if spec.VRF != "" { - vrfAvailable := false - for _, info := range availableVrfs { - if info.Name == spec.VRF { - vrfAvailable = true - break - } - } - if !vrfAvailable { - r.Logger.Error(err, "VRF of Layer2 not found on node", "layer", l2vnis[i].ObjectMeta.Name, "vrf", spec.VRF) - continue - } - } - - desired = append(desired, nl.Layer2Information{ - VlanID: spec.ID, - MTU: spec.MTU, - VNI: spec.VNI, - VRF: spec.VRF, - AnycastMAC: anycastMAC, - AnycastGateways: anycastGateways, - AdvertiseNeighbors: spec.AdvertiseNeighbors, - NeighSuppression: spec.NeighSuppression, - CreateMACVLANInterface: spec.CreateMACVLANInterface, - }) - } - - return desired, nil -} - -func determineToBeDeleted(existing, desired []nl.Layer2Information) []nl.Layer2Information { - toDelete := []nl.Layer2Information{} - for i := range existing { - stillExists := false - for j := range desired { - if desired[j].VlanID == existing[i].VlanID { - stillExists = true - break - } - } - if !stillExists { - toDelete = append(toDelete, existing[i]) - } - } - return toDelete -} - -func (r *reconcile) 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 { - return fmt.Errorf("error reconciling layer2 vlan %d vni %d: %w", desired.VlanID, desired.VNI, err) - } - if desired.AdvertiseNeighbors { - bridgeID, err := r.netlinkManager.GetBridgeID(desired) - if err != nil { - return fmt.Errorf("error getting bridge id for vlanId %d: %w", desired.VlanID, err) - } - *anycastTrackerInterfaces = append(*anycastTrackerInterfaces, bridgeID) - } - return nil -} - -func (*reconcile) 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 { - return fmt.Errorf("dupliate Layer2 ID found: %s %s", configs[i].ObjectMeta.Name, configs[j].ObjectMeta.Name) - } - if configs[i].Spec.VNI == configs[j].Spec.VNI { - return fmt.Errorf("dupliate Layer2 VNI found: %s %s", configs[i].ObjectMeta.Name, configs[j].ObjectMeta.Name) - } - } - } - return nil -} diff --git a/pkg/reconciler/nodenetworkconfig_reconciler.go b/pkg/reconciler/nodenetworkconfig_reconciler.go new file mode 100644 index 00000000..4962c525 --- /dev/null +++ b/pkg/reconciler/nodenetworkconfig_reconciler.go @@ -0,0 +1,252 @@ +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/config" + "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" + "github.com/telekom/das-schiff-network-operator/pkg/worker" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "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 + config *config.Config + logger logr.Logger + healthchecker healthcheck.Adapter + nodeNetworkConfig *v1alpha1.NodeNetworkConfig + nodeNetworkConfigPath string + workerClient worker.Client +} + +type reconcileNodeNetworkConfig struct { + *NodeNetworkConfigReconciler + logr.Logger +} + +func NewNodeNetworkConfigReconciler(clusterClient client.Client, logger logr.Logger, nodeNetworkConfigPath string, workerClient worker.Client, healtchecker healthcheck.Adapter) (*NodeNetworkConfigReconciler, error) { + reconciler := &NodeNetworkConfigReconciler{ + client: clusterClient, + logger: logger, + nodeNetworkConfigPath: nodeNetworkConfigPath, + workerClient: workerClient, + healthchecker: healtchecker, + } + + cfg, err := config.LoadConfig() + if err != nil { + return nil, fmt.Errorf("error loading config: %w", err) + } + reconciler.config = cfg + + 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) fetchNodeConfig(ctx context.Context) (*v1alpha1.NodeNetworkConfig, error) { + cfg := &v1alpha1.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 +} + +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 := r.doReconciliation(ctx, 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(ctx); err != nil { + return fmt.Errorf("error restoring NodeNetworkConfig: %w", err) + } + + return nil +} + +func (r *reconcileNodeNetworkConfig) doReconciliation(ctx context.Context, nodeCfg *v1alpha1.NodeNetworkConfig) error { + r.logger.Info("config to reconcile", "NodeNetworkConfig", *nodeCfg) + if err := r.workerClient.SendConfig(ctx, nodeCfg); err != nil { + return fmt.Errorf("failed to reconcile config: %w", err) + } + + return nil +} + +func (r *reconcileNodeNetworkConfig) restoreNodeNetworkConfig(ctx context.Context) error { + if r.nodeNetworkConfig == nil { + return nil + } + if err := r.doReconciliation(ctx, 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 { + 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 9f7dd945..00000000 --- a/pkg/reconciler/reconciler.go +++ /dev/null @@ -1,132 +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 - - dirtyFRRConfig bool -} - -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..8ecda8ed --- /dev/null +++ b/pkg/reconciler/reconciler_test.go @@ -0,0 +1,350 @@ +package reconciler + +import ( + "context" + "encoding/json" + "fmt" + "os" + "path/filepath" + "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" + "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" + mock_hc "github.com/telekom/das-schiff-network-operator/pkg/healthcheck/mock" + mock_worker "github.com/telekom/das-schiff-network-operator/pkg/worker/mock" + "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("create new reconciler", func() { + c := createClient() + r, err := NewNodeNetworkConfigReconciler(c, logr.New(nil), "", nil, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + }) + }) + Context("Reconcile() should", func() { + It("return no error if there is no config to reconcile", func() { + c := createClient() + r, err := NewNodeNetworkConfigReconciler(c, logr.New(nil), "", nil, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + }) + It("return error if worker fails to configure networking", func() { + c := createFullClient() + wc := mock_worker.NewMockClient(mockctrl) + wc.EXPECT().SendConfig(gomock.Any(), gomock.Any()).Return(fmt.Errorf("error")) + r, err := NewNodeNetworkConfigReconciler(c, logr.New(nil), "", wc, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + It("return error if healthcheck failed", func() { + c := createFullClient() + wc := mock_worker.NewMockClient(mockctrl) + hc := mock_hc.NewMockAdapter(mockctrl) + wc.EXPECT().SendConfig(gomock.Any(), gomock.Any()).Return(nil) + r, err := NewNodeNetworkConfigReconciler(c, logr.New(nil), "", wc, hc) + hc.EXPECT().CheckReachability().Return(fmt.Errorf("error")) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).To(HaveOccurred()) + }) + It("return no error", func() { + c := createFullClient() + wc := mock_worker.NewMockClient(mockctrl) + hc := mock_hc.NewMockAdapter(mockctrl) + wc.EXPECT().SendConfig(gomock.Any(), gomock.Any()).Return(nil) + r, err := NewNodeNetworkConfigReconciler(c, logr.New(nil), filepath.Join(tmpDir, "node.yaml"), wc, hc) + hc.EXPECT().CheckReachability().Return(nil) + hc.EXPECT().CheckAPIServer(gomock.Any()).Return(nil) + hc.EXPECT().TaintsRemoved().Return(true) + Expect(err).ToNot(HaveOccurred()) + Expect(r).ToNot(BeNil()) + err = r.Reconcile(context.TODO()) + Expect(err).ToNot(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 +} diff --git a/pkg/worker/mock/mock_worker.go b/pkg/worker/mock/mock_worker.go new file mode 100644 index 00000000..32421979 --- /dev/null +++ b/pkg/worker/mock/mock_worker.go @@ -0,0 +1,50 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/telekom/das-schiff-network-operator/pkg/worker (interfaces: Client) + +// Package mock_worker is a generated GoMock package. +package mock_worker + +import ( + context "context" + reflect "reflect" + + v1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + gomock "go.uber.org/mock/gomock" +) + +// MockClient is a mock of Client interface. +type MockClient struct { + ctrl *gomock.Controller + recorder *MockClientMockRecorder +} + +// MockClientMockRecorder is the mock recorder for MockClient. +type MockClientMockRecorder struct { + mock *MockClient +} + +// NewMockClient creates a new mock instance. +func NewMockClient(ctrl *gomock.Controller) *MockClient { + mock := &MockClient{ctrl: ctrl} + mock.recorder = &MockClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClient) EXPECT() *MockClientMockRecorder { + return m.recorder +} + +// SendConfig mocks base method. +func (m *MockClient) SendConfig(arg0 context.Context, arg1 *v1alpha1.NodeNetworkConfig) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SendConfig", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// SendConfig indicates an expected call of SendConfig. +func (mr *MockClientMockRecorder) SendConfig(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendConfig", reflect.TypeOf((*MockClient)(nil).SendConfig), arg0, arg1) +} diff --git a/pkg/worker/pb/worker.pb.go b/pkg/worker/pb/worker.pb.go new file mode 100644 index 00000000..9503d4bc --- /dev/null +++ b/pkg/worker/pb/worker.pb.go @@ -0,0 +1,219 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.33.0 +// protoc v5.27.0 +// source: pkg/worker/pb/worker.proto + +package agent + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NetworkConfiguration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *NetworkConfiguration) Reset() { + *x = NetworkConfiguration{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_worker_pb_worker_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *NetworkConfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NetworkConfiguration) ProtoMessage() {} + +func (x *NetworkConfiguration) ProtoReflect() protoreflect.Message { + mi := &file_pkg_worker_pb_worker_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NetworkConfiguration.ProtoReflect.Descriptor instead. +func (*NetworkConfiguration) Descriptor() ([]byte, []int) { + return file_pkg_worker_pb_worker_proto_rawDescGZIP(), []int{0} +} + +func (x *NetworkConfiguration) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +type Response struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Configured bool `protobuf:"varint,1,opt,name=configured,proto3" json:"configured,omitempty"` +} + +func (x *Response) Reset() { + *x = Response{} + if protoimpl.UnsafeEnabled { + mi := &file_pkg_worker_pb_worker_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Response) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Response) ProtoMessage() {} + +func (x *Response) ProtoReflect() protoreflect.Message { + mi := &file_pkg_worker_pb_worker_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Response.ProtoReflect.Descriptor instead. +func (*Response) Descriptor() ([]byte, []int) { + return file_pkg_worker_pb_worker_proto_rawDescGZIP(), []int{1} +} + +func (x *Response) GetConfigured() bool { + if x != nil { + return x.Configured + } + return false +} + +var File_pkg_worker_pb_worker_proto protoreflect.FileDescriptor + +var file_pkg_worker_pb_worker_proto_rawDesc = []byte{ + 0x0a, 0x1a, 0x70, 0x6b, 0x67, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2f, 0x70, 0x62, 0x2f, + 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x2a, 0x0a, 0x14, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x2a, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x65, 0x64, 0x32, 0x52, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, + 0x74, 0x12, 0x49, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x3a, 0x5a, 0x38, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x6b, + 0x6f, 0x6d, 0x2f, 0x64, 0x61, 0x73, 0x2d, 0x73, 0x63, 0x68, 0x69, 0x66, 0x66, 0x2d, 0x6e, 0x65, + 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2d, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x6f, 0x72, 0x2f, 0x70, + 0x6b, 0x67, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_pkg_worker_pb_worker_proto_rawDescOnce sync.Once + file_pkg_worker_pb_worker_proto_rawDescData = file_pkg_worker_pb_worker_proto_rawDesc +) + +func file_pkg_worker_pb_worker_proto_rawDescGZIP() []byte { + file_pkg_worker_pb_worker_proto_rawDescOnce.Do(func() { + file_pkg_worker_pb_worker_proto_rawDescData = protoimpl.X.CompressGZIP(file_pkg_worker_pb_worker_proto_rawDescData) + }) + return file_pkg_worker_pb_worker_proto_rawDescData +} + +var file_pkg_worker_pb_worker_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_pkg_worker_pb_worker_proto_goTypes = []interface{}{ + (*NetworkConfiguration)(nil), // 0: agent.NetworkConfiguration + (*Response)(nil), // 1: agent.Response + (*emptypb.Empty)(nil), // 2: google.protobuf.Empty +} +var file_pkg_worker_pb_worker_proto_depIdxs = []int32{ + 0, // 0: agent.Agent.SetConfiguration:input_type -> agent.NetworkConfiguration + 2, // 1: agent.Agent.SetConfiguration:output_type -> google.protobuf.Empty + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_pkg_worker_pb_worker_proto_init() } +func file_pkg_worker_pb_worker_proto_init() { + if File_pkg_worker_pb_worker_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_worker_pb_worker_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*NetworkConfiguration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_worker_pb_worker_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Response); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_worker_pb_worker_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_pkg_worker_pb_worker_proto_goTypes, + DependencyIndexes: file_pkg_worker_pb_worker_proto_depIdxs, + MessageInfos: file_pkg_worker_pb_worker_proto_msgTypes, + }.Build() + File_pkg_worker_pb_worker_proto = out.File + file_pkg_worker_pb_worker_proto_rawDesc = nil + file_pkg_worker_pb_worker_proto_goTypes = nil + file_pkg_worker_pb_worker_proto_depIdxs = nil +} diff --git a/pkg/worker/pb/worker.proto b/pkg/worker/pb/worker.proto new file mode 100644 index 00000000..a67faf55 --- /dev/null +++ b/pkg/worker/pb/worker.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +option go_package = "github.com/telekom/das-schiff-network-operator/pkg/agent"; + +import "google/protobuf/empty.proto"; + +package agent; + +service Agent { + rpc SetConfiguration(NetworkConfiguration) returns (google.protobuf.Empty) {} +} + +message NetworkConfiguration { + bytes data = 1; +} + +message Response { + bool configured = 1; +} \ No newline at end of file diff --git a/pkg/worker/pb/worker_grpc.pb.go b/pkg/worker/pb/worker_grpc.pb.go new file mode 100644 index 00000000..be70ac0b --- /dev/null +++ b/pkg/worker/pb/worker_grpc.pb.go @@ -0,0 +1,110 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.3.0 +// - protoc v5.27.0 +// source: pkg/worker/pb/worker.proto + +package agent + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +const ( + Agent_SetConfiguration_FullMethodName = "/agent.Agent/SetConfiguration" +) + +// AgentClient is the client API for Agent service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AgentClient interface { + SetConfiguration(ctx context.Context, in *NetworkConfiguration, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type agentClient struct { + cc grpc.ClientConnInterface +} + +func NewAgentClient(cc grpc.ClientConnInterface) AgentClient { + return &agentClient{cc} +} + +func (c *agentClient) SetConfiguration(ctx context.Context, in *NetworkConfiguration, opts ...grpc.CallOption) (*emptypb.Empty, error) { + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, Agent_SetConfiguration_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AgentServer is the server API for Agent service. +// All implementations must embed UnimplementedAgentServer +// for forward compatibility +type AgentServer interface { + SetConfiguration(context.Context, *NetworkConfiguration) (*emptypb.Empty, error) + mustEmbedUnimplementedAgentServer() +} + +// UnimplementedAgentServer must be embedded to have forward compatible implementations. +type UnimplementedAgentServer struct { +} + +func (UnimplementedAgentServer) SetConfiguration(context.Context, *NetworkConfiguration) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetConfiguration not implemented") +} +func (UnimplementedAgentServer) mustEmbedUnimplementedAgentServer() {} + +// UnsafeAgentServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AgentServer will +// result in compilation errors. +type UnsafeAgentServer interface { + mustEmbedUnimplementedAgentServer() +} + +func RegisterAgentServer(s grpc.ServiceRegistrar, srv AgentServer) { + s.RegisterService(&Agent_ServiceDesc, srv) +} + +func _Agent_SetConfiguration_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NetworkConfiguration) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AgentServer).SetConfiguration(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: Agent_SetConfiguration_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AgentServer).SetConfiguration(ctx, req.(*NetworkConfiguration)) + } + return interceptor(ctx, in, info, handler) +} + +// Agent_ServiceDesc is the grpc.ServiceDesc for Agent service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var Agent_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "agent.Agent", + HandlerType: (*AgentServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SetConfiguration", + Handler: _Agent_SetConfiguration_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "pkg/worker/pb/worker.proto", +} diff --git a/pkg/worker/server.go b/pkg/worker/server.go new file mode 100644 index 00000000..f199b4bb --- /dev/null +++ b/pkg/worker/server.go @@ -0,0 +1,87 @@ +package worker + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/go-logr/logr" + "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/config" + workerpb "github.com/telekom/das-schiff-network-operator/pkg/worker/pb" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" +) + +const ( + DefaultPort = 50042 +) + +type Server struct { + adapter Adapter + workerpb.UnimplementedAgentServer + logger *logr.Logger +} + +type Adapter interface { + ReconcileLayer3([]v1alpha1.VRFRouteConfigurationSpec, []v1alpha1.RoutingTableSpec) error + ReconcileLayer2([]v1alpha1.Layer2NetworkConfigurationSpec) error + CheckHealth() error + GetConfig() *config.Config +} + +//go:generate mockgen -destination ./mock/mock_worker.go . Client +type Client interface { + SendConfig(context.Context, *v1alpha1.NodeNetworkConfig) error +} + +func NewServer(adapter Adapter, logger *logr.Logger) *Server { + sLog := logger.WithName("worker-server") + return &Server{ + adapter: adapter, + logger: &sLog, + } +} + +// nolint: wrapcheck +func (s Server) SetConfiguration(_ context.Context, nc *workerpb.NetworkConfiguration) (*emptypb.Empty, error) { + s.logger.Info("new request") + if nc == nil { + s.logger.Info("nil request") + return &emptypb.Empty{}, status.Error(http.StatusBadRequest, "got nil request") + } + + var nodeCfg v1alpha1.NodeNetworkConfig + + s.logger.Info("umarshaling data...") + if err := json.Unmarshal(nc.Data, &nodeCfg); err != nil { + s.logger.Error(fmt.Errorf("error unmrashalling NodeConfig objec"), "error") + return &emptypb.Empty{}, status.Error(http.StatusInternalServerError, "error unmrashalling NodeConfig object") + } + + if err := s.adapter.GetConfig().ReloadConfig(); err != nil { + s.logger.Error(fmt.Errorf("error reloading configc"), "error") + return &emptypb.Empty{}, status.Error(http.StatusInternalServerError, "error reloading config") + } + + s.logger.Info("reconciling config...", "config", nodeCfg) + + l3vnis := nodeCfg.Spec.Vrf + l2vnis := nodeCfg.Spec.Layer2 + taas := nodeCfg.Spec.RoutingTable + + if err := s.adapter.ReconcileLayer3(l3vnis, taas); err != nil { + return &emptypb.Empty{}, status.Errorf(http.StatusInternalServerError, "error configuring Layer3: %s", err.Error()) + } + if err := s.adapter.ReconcileLayer2(l2vnis); err != nil { + return &emptypb.Empty{}, status.Errorf(http.StatusInternalServerError, "error configuring Layer2: %s", err.Error()) + } + if err := s.adapter.CheckHealth(); err != nil { + return &emptypb.Empty{}, status.Errorf(http.StatusInternalServerError, "healthcheck error: %s", err.Error()) + } + + s.logger.Info("config reconciled successfully") + + return &emptypb.Empty{}, nil +} diff --git a/worker.Dockerfile b/worker.Dockerfile new file mode 100644 index 00000000..ecd718de --- /dev/null +++ b/worker.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/worker/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 worker main.go + +FROM alpine:latest + +RUN apk add --no-cache iptables ip6tables + +WORKDIR / +COPY --from=builder /workspace/worker . +USER 65532:65532 + +ENTRYPOINT ["/worker"] \ No newline at end of file