diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index f1aeec9..cf90f96 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -23,30 +23,18 @@ jobs: - name: kubebuilder tests run: make test - staticcheck: - runs-on: ubuntu-20.04 - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-go@v2 - with: - go-version: ${{ env.GOVER }} - - name: Staticcheck - run: | - go install honnef.co/go/tools/cmd/staticcheck@latest - staticcheck ./... - - golang-ci: + golang-ci-lint: runs-on: ubuntu-20.04 steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 0 - uses: actions/setup-go@v2 with: go-version: ${{ env.GOVER }} - name: golangci-lint - uses: golangci/golangci-lint-action@v2 + uses: golangci/golangci-lint-action@v3 with: - args: --timeout=3m - skip-go-installation: true + args: --config=./.github/workflows/linters/.golangci.yml + version: "v1.47.2" diff --git a/.github/workflows/linters/.golangci.yml b/.github/workflows/linters/.golangci.yml new file mode 100644 index 0000000..25836d5 --- /dev/null +++ b/.github/workflows/linters/.golangci.yml @@ -0,0 +1,77 @@ +run: + # running w/ 1.17 because we dont actually need/use 1.18 things and 1.18 breaks some linters. + go: "1.17" + timeout: 5m + skip-dirs: + - private + - bin + +output: + sort-results: true + +linters: + enable: + - nlreturn + - forbidigo + - gofumpt + - bodyclose + - deadcode + - depguard + - dogsled + - dupl + - errcheck + - exhaustive + - funlen + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - noctx + - nolintlint + - revive + - rowserrcheck + - exportloopref + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - whitespace + - asciicheck + - gochecknoglobals + - gocognit + - godot + - godox + - goerr113 + - nestif + - prealloc + - testpackage + - wsl +linters-settings: + lll: + line-length: 140 + +issues: + # https://github.com/golangci/golangci-lint/issues/2439#issuecomment-1002912465 + exclude-use-default: false + exclude-rules: + - path: _test\.go + linters: + - gomnd + - dupl + - structcheck + - unused + - unparam diff --git a/.mk/lint.mk b/.mk/lint.mk new file mode 100644 index 0000000..6a1ad93 --- /dev/null +++ b/.mk/lint.mk @@ -0,0 +1,32 @@ +GOFUMPT_CMD := docker run --rm -it -v $(shell pwd):/work ghcr.io/hellt/gofumpt:0.3.1 +GOFUMPT_FLAGS := -l -w . + +GODOT_CMD := docker run --rm -it -v $(shell pwd):/work ghcr.io/hellt/godot:1.4.11 +GODOT_FLAGS := -w . + +GOLINES_CMD := docker run --rm -it -v $(shell pwd):/work ghcr.io/hellt/golines:0.10.0 golines +GOLINES_FLAGS := -w . + +GOLANGCI_CMD := docker run -it --rm -v $$(pwd):/app -w /app golangci/golangci-lint:v1.47.2 golangci-lint +GOLANGCI_FLAGS := --config ./.github/workflows/linters/.golangci.yml run -v --fix + + +# when running in a CI env we use locally installed bind +ifdef CI + GOFUMPT_CMD := gofumpt +endif + + +format: gofumpt godot golines # apply Go formatters + +gofumpt: + ${GOFUMPT_CMD} ${GOFUMPT_FLAGS} + +godot: + ${GODOT_CMD} ${GODOT_FLAGS} + +golines: + ${GOLINES_CMD} ${GOLINES_FLAGS} + +golangci: # linting with golang-ci lint container + ${GOLANGCI_CMD} ${GOLANGCI_FLAGS} \ No newline at end of file diff --git a/Makefile b/Makefile index cd49a9a..7e96a05 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ +include .mk/lint.mk # Image URL to use all building/pushing image targets IMG ?= controller:latest @@ -84,28 +85,22 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. $(KUSTOMIZE) build config/default | kubectl delete -f - - +# check source for latest methods on getting these binaries +# https://github.com/kubernetes-sigs/kubebuilder/blob/95f55fcf8b87397f7fdf97456b10ff70b31f728e/pkg/plugins/golang/v3/scaffolds/internal/templates/makefile.go CONTROLLER_GEN = $(shell pwd)/bin/controller-gen controller-gen: ## Download controller-gen locally if necessary. - $(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1) + $(call go-install,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen@v0.4.1) KUSTOMIZE = $(shell pwd)/bin/kustomize kustomize: ## Download kustomize locally if necessary. - $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) + $(call go-install,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v3@v3.8.7) -# go-get-tool will 'go get' any package $2 and install it to $1. -PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) -define go-get-tool +# go-install will 'go get' any package $2 and install it to $1. +PROJECT_DIR := $(shell dirname $(abspath $(firstword $(MAKEFILE_LIST)))) +define go-install @[ -f $(1) ] || { \ set -e ;\ -TMP_DIR=$$(mktemp -d) ;\ -cd $$TMP_DIR ;\ -go mod init tmp ;\ echo "Downloading $(2)" ;\ -GOBIN=$(PROJECT_DIR)/bin go get $(2) ;\ -rm -rf $$TMP_DIR ;\ +GOBIN=$(PROJECT_DIR)/bin go install $(2) ;\ } endef - -ci-lint: # linting with golang-ci lint - docker run -it --rm -v $$(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 2m \ No newline at end of file diff --git a/api/clientset/v1alpha1/srlinux.go b/api/clientset/v1alpha1/srlinux.go index e21463c..d702ef6 100644 --- a/api/clientset/v1alpha1/srlinux.go +++ b/api/clientset/v1alpha1/srlinux.go @@ -1,7 +1,11 @@ +// Package v1alpha1 is an v1alpha version of a Clientset for SR Linux customer resource. package v1alpha1 +// note to my future self: see https://www.martin-helmich.de/en/blog/kubernetes-crd-client.html for details + import ( "context" + "errors" "fmt" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,6 +20,9 @@ import ( typesv1alpha1 "github.com/srl-labs/srl-controller/api/types/v1alpha1" ) +// ErrUpdateFailed occurs when update operation fails on srlinux CR. +var ErrUpdateFailed = errors.New("operation update failed") + // SrlinuxInterface provides access to the Srlinux CRD. type SrlinuxInterface interface { List(ctx context.Context, opts metav1.ListOptions) (*typesv1alpha1.SrlinuxList, error) @@ -23,8 +30,17 @@ type SrlinuxInterface interface { Create(ctx context.Context, srlinux *typesv1alpha1.Srlinux) (*typesv1alpha1.Srlinux, error) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) - Unstructured(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) - Update(ctx context.Context, obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*typesv1alpha1.Srlinux, error) + Unstructured( + ctx context.Context, + name string, + opts metav1.GetOptions, + subresources ...string, + ) (*unstructured.Unstructured, error) + Update( + ctx context.Context, + obj *unstructured.Unstructured, + opts metav1.UpdateOptions, + ) (*typesv1alpha1.Srlinux, error) } // Interface is the clientset interface for srlinux. @@ -38,7 +54,7 @@ type Clientset struct { restClient rest.Interface } -var gvr = schema.GroupVersionResource{ +var gvr = schema.GroupVersionResource{ // nolint: gochecknoglobals Group: typesv1alpha1.GroupVersion.Group, Version: typesv1alpha1.GroupVersion.Version, Resource: "srlinuxes", @@ -51,21 +67,26 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { config.APIPath = "/apis" config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() config.UserAgent = rest.DefaultKubernetesUserAgent() + dClient, err := dynamic.NewForConfig(c) if err != nil { return nil, err } + dInterface := dClient.Resource(gvr) + rClient, err := rest.RESTClientFor(&config) if err != nil { return nil, err } + return &Clientset{ dInterface: dInterface, restClient: rClient, }, nil } +// Srlinux initializes srlinuxClient struct which implements SrlinuxInterface. func (c *Clientset) Srlinux(namespace string) SrlinuxInterface { return &srlinuxClient{ dInterface: c.dInterface, @@ -80,7 +101,11 @@ type srlinuxClient struct { ns string } -func (s *srlinuxClient) List(ctx context.Context, opts metav1.ListOptions) (*typesv1alpha1.SrlinuxList, error) { +// List gets a list of SRLinux resources. +func (s *srlinuxClient) List( + ctx context.Context, + opts metav1.ListOptions, +) (*typesv1alpha1.SrlinuxList, error) { result := typesv1alpha1.SrlinuxList{} err := s.restClient. Get(). @@ -93,7 +118,12 @@ func (s *srlinuxClient) List(ctx context.Context, opts metav1.ListOptions) (*typ return &result, err } -func (s *srlinuxClient) Get(ctx context.Context, name string, opts metav1.GetOptions) (*typesv1alpha1.Srlinux, error) { +// Get gets SRLinux resource. +func (s *srlinuxClient) Get( + ctx context.Context, + name string, + opts metav1.GetOptions, +) (*typesv1alpha1.Srlinux, error) { result := typesv1alpha1.Srlinux{} err := s.restClient. Get(). @@ -107,7 +137,11 @@ func (s *srlinuxClient) Get(ctx context.Context, name string, opts metav1.GetOpt return &result, err } -func (s *srlinuxClient) Create(ctx context.Context, srlinux *typesv1alpha1.Srlinux) (*typesv1alpha1.Srlinux, error) { +// Create creates SRLinux resource. +func (s *srlinuxClient) Create( + ctx context.Context, + srlinux *typesv1alpha1.Srlinux, +) (*typesv1alpha1.Srlinux, error) { result := typesv1alpha1.Srlinux{} err := s.restClient. Post(). @@ -120,8 +154,12 @@ func (s *srlinuxClient) Create(ctx context.Context, srlinux *typesv1alpha1.Srlin return &result, err } -func (s *srlinuxClient) Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) { +func (s *srlinuxClient) Watch( + ctx context.Context, + opts metav1.ListOptions, +) (watch.Interface, error) { opts.Watch = true + return s.restClient. Get(). Namespace(s.ns). @@ -130,10 +168,10 @@ func (s *srlinuxClient) Watch(ctx context.Context, opts metav1.ListOptions) (wat Watch(ctx) } -func (t *srlinuxClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { - return t.restClient. +func (s *srlinuxClient) Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error { + return s.restClient. Delete(). - Namespace(t.ns). + Namespace(s.ns). Resource(gvr.Resource). VersionedParams(&opts, scheme.ParameterCodec). Name(name). @@ -141,20 +179,32 @@ func (t *srlinuxClient) Delete(ctx context.Context, name string, opts metav1.Del Error() } -func (s *srlinuxClient) Update(ctx context.Context, obj *unstructured.Unstructured, opts metav1.UpdateOptions) (*typesv1alpha1.Srlinux, error) { +func (s *srlinuxClient) Update( + ctx context.Context, + obj *unstructured.Unstructured, + opts metav1.UpdateOptions, +) (*typesv1alpha1.Srlinux, error) { result := typesv1alpha1.Srlinux{} + obj, err := s.dInterface.Namespace(s.ns).UpdateStatus(ctx, obj, metav1.UpdateOptions{}) if err != nil { return nil, err } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &result) if err != nil { - return nil, fmt.Errorf("failed to type assert return to srlinux") + return nil, fmt.Errorf("failed to type assert return to srlinux: %w", ErrUpdateFailed) } + return &result, nil } -func (s *srlinuxClient) Unstructured(ctx context.Context, name string, opts metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error) { +func (s *srlinuxClient) Unstructured( + ctx context.Context, + name string, + opts metav1.GetOptions, + subresources ...string, +) (*unstructured.Unstructured, error) { return s.dInterface.Namespace(s.ns).Get(ctx, name, opts, subresources...) } diff --git a/api/types/v1alpha1/groupversion_info.go b/api/types/v1alpha1/groupversion_info.go index 6ce47a0..35e47e7 100644 --- a/api/types/v1alpha1/groupversion_info.go +++ b/api/types/v1alpha1/groupversion_info.go @@ -15,8 +15,8 @@ limitations under the License. */ // Package v1alpha1 contains API Schema definitions for the kne v1alpha1 API group -//+kubebuilder:object:generate=true -//+groupName=kne.srlinux.dev +// +kubebuilder:object:generate=true +// +groupName=kne.srlinux.dev package v1alpha1 import ( @@ -25,12 +25,15 @@ import ( ) var ( - // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "kne.srlinux.dev", Version: "v1alpha1"} + // GroupVersion is group version used to register these objects. + GroupVersion = schema.GroupVersion{ // nolint: gochecknoglobals + Group: "kne.srlinux.dev", + Version: "v1alpha1", + } - // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + // SchemeBuilder is used to add go types to the GroupVersionKind scheme. + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} // nolint: gochecknoglobals // AddToScheme adds the types in this group-version to the given scheme. - AddToScheme = SchemeBuilder.AddToScheme + AddToScheme = SchemeBuilder.AddToScheme // nolint: gochecknoglobals ) diff --git a/api/types/v1alpha1/srlinux_types.go b/api/types/v1alpha1/srlinux_types.go index 9d2167d..70e3aa7 100644 --- a/api/types/v1alpha1/srlinux_types.go +++ b/api/types/v1alpha1/srlinux_types.go @@ -20,9 +20,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// SrlinuxSpec defines the desired state of Srlinux +// SrlinuxSpec defines the desired state of Srlinux. type SrlinuxSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "make" to regenerate code after modifying this file Config *NodeConfig `json:"config,omitempty"` @@ -32,18 +31,18 @@ type SrlinuxSpec struct { Version string `json:"version,omitempty"` } -// SrlinuxStatus defines the observed state of Srlinux +// SrlinuxStatus defines the observed state of Srlinux. type SrlinuxStatus struct { // Image used to run srlinux pod Image string `json:"image,omitempty"` } -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status // Srlinux is the Schema for the srlinuxes API -//+kubebuilder:printcolumn:name="Image",type="string",JSONPath=".status.image" -//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=".status.image" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" type Srlinux struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -54,7 +53,7 @@ type Srlinux struct { //+kubebuilder:object:root=true -// SrlinuxList contains a list of Srlinux +// SrlinuxList contains a list of Srlinux. type SrlinuxList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` @@ -65,6 +64,7 @@ func init() { SchemeBuilder.Register(&Srlinux{}, &SrlinuxList{}) } +// GetConfig gets config from srlinux spec. func (s *SrlinuxSpec) GetConfig() *NodeConfig { if s.Config != nil { return s.Config @@ -73,6 +73,8 @@ func (s *SrlinuxSpec) GetConfig() *NodeConfig { return nil } +// GetConstraints gets constraints from srlinux spec, +// default constraints are returned if none are present in the spec. func (s *SrlinuxSpec) GetConstraints() map[string]string { if s.Constraints != nil { return s.Constraints @@ -81,6 +83,8 @@ func (s *SrlinuxSpec) GetConstraints() map[string]string { return defaultConstraints } +// GetModel gets srlinux model (aka variant) from srlinux spec, +// default srlinux variant is returned if none present in the spec. func (s *SrlinuxSpec) GetModel() string { if s.Model != "" { return s.Model @@ -91,7 +95,7 @@ func (s *SrlinuxSpec) GetModel() string { // GetImage returns the srlinux container image name that is used in pod spec // if Config.Image is provided it takes precedence over all other option -// if not, the Spec.Version is used as a tag for public container image ghcr.io/nokia/srlinux +// if not, the Spec.Version is used as a tag for public container image ghcr.io/nokia/srlinux. func (s *SrlinuxSpec) GetImage() string { if s.GetConfig().Image != "" { return s.GetConfig().Image diff --git a/api/types/v1alpha1/types.go b/api/types/v1alpha1/types.go index 35dfa4d..53d6779 100644 --- a/api/types/v1alpha1/types.go +++ b/api/types/v1alpha1/types.go @@ -6,6 +6,7 @@ const ( ) var ( + // nolint: gochecknoglobals defaultCmd = []string{ "/tini", "--", @@ -14,6 +15,7 @@ var ( "/kne-entrypoint.sh", } + // nolint: gochecknoglobals defaultArgs = []string{ "sudo", "bash", @@ -21,12 +23,14 @@ var ( "touch /.dockerenv && /opt/srlinux/bin/sr_linux", } + // nolint: gochecknoglobals defaultConstraints = map[string]string{ "cpu": "0.5", "memory": "1Gi", } ) +// NodeConfig represents srlinux node configuration parameters. type NodeConfig struct { Command []string `json:"command,omitempty"` // Command to pass into pod. Args []string `json:"args,omitempty"` // Command args to pass into the pod. @@ -42,9 +46,10 @@ type NodeConfig struct { // When set to true by kne, srlinux controller will attempt to mount the file with startup config to the pod ConfigDataPresent bool `json:"config_data_present,omitempty"` Cert *CertificateCfg `json:"cert,omitempty"` - Sleep uint32 `json:"sleep,omitempty"` // Sleeptime before starting the pod. + Sleep uint32 `json:"sleep,omitempty"` // Sleep time before starting the pod. } +// CertificateCfg represents srlinux certificate configuration parameters. type CertificateCfg struct { // Certificate name on the node. CertName string `json:"cert_name,omitempty"` @@ -56,6 +61,7 @@ type CertificateCfg struct { CommonName string `json:"common_name,omitempty"` } +// GetCommand gets command from srlinux node configuration. func (n *NodeConfig) GetCommand() []string { if n.Command != nil { return n.Command @@ -64,6 +70,7 @@ func (n *NodeConfig) GetCommand() []string { return defaultCmd } +// GetArgs gets arguments from srlinux node configuration. func (n *NodeConfig) GetArgs() []string { if n.Args != nil { return n.Args diff --git a/config/crd/bases/kne.srlinux.dev_srlinuxes.yaml b/config/crd/bases/kne.srlinux.dev_srlinuxes.yaml index 26e5781..6bf9ac0 100644 --- a/config/crd/bases/kne.srlinux.dev_srlinuxes.yaml +++ b/config/crd/bases/kne.srlinux.dev_srlinuxes.yaml @@ -41,15 +41,18 @@ spec: metadata: type: object spec: - description: SrlinuxSpec defines the desired state of Srlinux + description: SrlinuxSpec defines the desired state of Srlinux. properties: config: + description: NodeConfig represents srlinux node configuration parameters. properties: args: items: type: string type: array cert: + description: CertificateCfg represents srlinux certificate configuration + parameters. properties: cert_name: description: Certificate name on the node. @@ -107,7 +110,7 @@ spec: type: string type: object status: - description: SrlinuxStatus defines the observed state of Srlinux + description: SrlinuxStatus defines the observed state of Srlinux. properties: image: description: Image used to run srlinux pod diff --git a/controllers/cfgmap.go b/controllers/cfgmap.go new file mode 100644 index 0000000..2a6930c --- /dev/null +++ b/controllers/cfgmap.go @@ -0,0 +1,143 @@ +package controllers + +import ( + "context" + + "github.com/go-logr/logr" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" +) + +// createConfigMaps creates srlinux-variants and srlinux-topomac config maps which every srlinux pod needs to mount. +func createConfigMaps( + ctx context.Context, + r *SrlinuxReconciler, + ns string, + log logr.Logger, +) error { + err := createVariantsCfgMap(ctx, r, ns, log) + if err != nil { + return err + } + + err = createTopomacScriptCfgMap(ctx, r, ns, log) + if err != nil { + return err + } + + err = createKNEEntrypointCfgMap(ctx, r, ns, log) + + return err +} + +func createVariantsCfgMap( + ctx context.Context, + r *SrlinuxReconciler, + ns string, + log logr.Logger, +) error { + // Check if the variants cfg map already exists, if not create a new one + cfgMap := &corev1.ConfigMap{} + + err := r.Get(ctx, types.NamespacedName{Name: variantsCfgMapName, Namespace: ns}, cfgMap) + if err != nil && errors.IsNotFound(err) { + log.Info("Creating a new variants configmap") + + data, err := VariantsFS.ReadFile("manifests/variants/srl_variants.yml") + if err != nil { + return err + } + + decoder := serializer.NewCodecFactory(clientgoscheme.Scheme).UniversalDecoder() + + err = runtime.DecodeInto(decoder, data, cfgMap) + if err != nil { + return err + } + + cfgMap.ObjectMeta.Namespace = ns + + err = r.Create(ctx, cfgMap) + if err != nil { + return err + } + } + + return err +} + +func createTopomacScriptCfgMap( + ctx context.Context, + r *SrlinuxReconciler, + ns string, + log logr.Logger, +) error { + // Check if the topomac script cfg map already exists, if not create a new one + cfgMap := &corev1.ConfigMap{} + + err := r.Get(ctx, types.NamespacedName{Name: topomacCfgMapName, Namespace: ns}, cfgMap) + if err != nil && errors.IsNotFound(err) { + log.Info("Creating a new topomac script configmap") + + data, err := VariantsFS.ReadFile("manifests/variants/topomac.yml") + if err != nil { + return err + } + + decoder := serializer.NewCodecFactory(clientgoscheme.Scheme).UniversalDecoder() + + err = runtime.DecodeInto(decoder, data, cfgMap) + if err != nil { + return err + } + + cfgMap.ObjectMeta.Namespace = ns + + err = r.Create(ctx, cfgMap) + if err != nil { + return err + } + } + + return err +} + +func createKNEEntrypointCfgMap( + ctx context.Context, + r *SrlinuxReconciler, + ns string, + log logr.Logger, +) error { + // Check if the kne-entrypoint cfg map already exists, if not create a new one + cfgMap := &corev1.ConfigMap{} + + err := r.Get(ctx, types.NamespacedName{Name: entrypointCfgMapName, Namespace: ns}, cfgMap) + if err != nil && errors.IsNotFound(err) { + log.Info("Creating a new kne-entrypoint configmap") + + data, err := VariantsFS.ReadFile("manifests/variants/kne-entrypoint.yml") + if err != nil { + return err + } + + decoder := serializer.NewCodecFactory(clientgoscheme.Scheme).UniversalDecoder() + + err = runtime.DecodeInto(decoder, data, cfgMap) + if err != nil { + return err + } + + cfgMap.ObjectMeta.Namespace = ns + + err = r.Create(ctx, cfgMap) + if err != nil { + return err + } + } + + return err +} diff --git a/controllers/pod.go b/controllers/pod.go new file mode 100644 index 0000000..413a1bc --- /dev/null +++ b/controllers/pod.go @@ -0,0 +1,212 @@ +package controllers + +import ( + "context" + "fmt" + + "github.com/go-logr/logr" + knenode "github.com/openconfig/kne/topo/node" + typesv1a1 "github.com/srl-labs/srl-controller/api/types/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/pointer" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +const terminationGracePeriodSeconds = 0 + +// podForSrlinux returns a srlinux Pod object. +func (r *SrlinuxReconciler) podForSrlinux( + ctx context.Context, + s *typesv1a1.Srlinux, +) *corev1.Pod { + log := log.FromContext(ctx) + + if s.Spec.Config.Env == nil { + s.Spec.Config.Env = map[string]string{} + } + + s.Spec.Config.Env["SRLINUX"] = "1" // set default srlinux env var + + pod := &corev1.Pod{ + ObjectMeta: createObjectMeta(s), + Spec: corev1.PodSpec{ + InitContainers: createInitContainers(s), + Containers: createContainers(s), + TerminationGracePeriodSeconds: pointer.Int64(terminationGracePeriodSeconds), + NodeSelector: map[string]string{}, + Affinity: createAffinity(s), + Volumes: createVolumes(s), + }, + } + + // handle startup config volume mounts if the startup config was defined + handleStartupConfig(s, pod, log) + + _ = ctrl.SetControllerReference(s, pod, r.Scheme) + + return pod +} + +func createObjectMeta(s *typesv1a1.Srlinux) metav1.ObjectMeta { + return metav1.ObjectMeta{ + Name: s.Name, + Namespace: s.Namespace, + Labels: map[string]string{ + "app": s.Name, + "topo": s.Namespace, + }, + } +} + +func createInitContainers(s *typesv1a1.Srlinux) []corev1.Container { + return []corev1.Container{{ + Name: fmt.Sprintf("init-%s", s.Name), + Image: initContainerName, + Args: []string{ + fmt.Sprintf("%d", s.Spec.NumInterfaces+1), + fmt.Sprintf("%d", s.Spec.Config.Sleep), + }, + ImagePullPolicy: "IfNotPresent", + }} +} + +func createContainers(s *typesv1a1.Srlinux) []corev1.Container { + return []corev1.Container{{ + Name: s.Name, + Image: s.Spec.GetImage(), + Command: s.Spec.Config.GetCommand(), + Args: s.Spec.Config.GetArgs(), + Env: knenode.ToEnvVar(s.Spec.Config.Env), + Resources: knenode.ToResourceRequirements(s.Spec.GetConstraints()), + ImagePullPolicy: "IfNotPresent", + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.Bool(true), + RunAsUser: pointer.Int64(0), + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: variantsVolName, + MountPath: variantsVolMntPath, + }, + { + Name: topomacVolName, + MountPath: topomacVolMntPath, + }, + { + Name: entrypointVolName, + MountPath: entrypointVolMntPath, + SubPath: entrypointVolMntSubPath, + }, + }, + }} +} + +func createAffinity(s *typesv1a1.Srlinux) *corev1.Affinity { + return &corev1.Affinity{ + PodAntiAffinity: &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{ + { + Weight: srlinuxPodAffinityWeight, + PodAffinityTerm: corev1.PodAffinityTerm{ + LabelSelector: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{{ + Key: "topo", + Operator: "In", + Values: []string{s.Name}, + }}, + }, + TopologyKey: "kubernetes.io/hostname", + }, + }, + }, + }, + } +} + +func createVolumes(s *typesv1a1.Srlinux) []corev1.Volume { + return []corev1.Volume{ + { + Name: variantsVolName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: variantsCfgMapName, + }, + Items: []corev1.KeyToPath{ + { + Key: s.Spec.GetModel(), + Path: variantsTemplateTempName, + }, + }, + }, + }, + }, + { + Name: topomacVolName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: topomacCfgMapName, + }, + }, + }, + }, + { + Name: entrypointVolName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: entrypointCfgMapName, + }, + DefaultMode: pointer.Int32(fileMode777), + }, + }, + }, + } +} + +// handleStartupConfig creates volume mounts and volumes for srlinux pod +// if the config file was provided in the spec. +func handleStartupConfig(s *typesv1a1.Srlinux, pod *corev1.Pod, log logr.Logger) { + // initialize config path and config file variables + cfgPath := defaultConfigPath + if p := s.Spec.GetConfig().ConfigPath; p != "" { + cfgPath = p + } + + cfgFile := s.Spec.GetConfig().ConfigFile + + // only create startup config mounts if the config data was set in kne + if s.Spec.Config.ConfigDataPresent { + log.Info( + "Adding volume for startup config to pod spec", + "volume.name", + "startup-config-volume", + "mount.path", + cfgPath+"/"+cfgFile, + ) + + pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ + Name: "startup-config-volume", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-config", s.Name), + }, + }, + }, + }) + + pod.Spec.Containers[0].VolumeMounts = append( + pod.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: "startup-config-volume", + MountPath: cfgPath + "/" + cfgFile, + SubPath: cfgFile, + ReadOnly: true, + }, + ) + } +} diff --git a/controllers/srlinux_controller.go b/controllers/srlinux_controller.go index 2fed24e..32d952f 100644 --- a/controllers/srlinux_controller.go +++ b/controllers/srlinux_controller.go @@ -14,33 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package controllers contains srlinux k8s/kne controller code package controllers import ( "context" "embed" - "fmt" "reflect" - "github.com/go-logr/logr" - knenode "github.com/openconfig/kne/topo/node" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/types" - clientgoscheme "k8s.io/client-go/kubernetes/scheme" - "k8s.io/utils/pointer" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/go-logr/logr" typesv1alpha1 "github.com/srl-labs/srl-controller/api/types/v1alpha1" ) const ( - InitContainerName = "networkop/init-wait:latest" + initContainerName = "networkop/init-wait:latest" variantsVolName = "variants" variantsVolMntPath = "/tmp/topo" variantsTemplateTempName = "topo-template.yml" @@ -56,313 +51,156 @@ const ( entrypointCfgMapName = "srlinux-kne-entrypoint" // default path to a startup config file - // the default for config file name resides within kne + // the default for config file name resides within kne. defaultConfigPath = "/etc/opt/srlinux" -) -var ( - VariantsFS embed.FS + fileMode777 = 0o777 + + srlinuxPodAffinityWeight = 100 ) -// SrlinuxReconciler reconciles a Srlinux object +// VariantsFS is variable without fs assignment, since it is used in main.go +// to assign a value for an fs that is in the outer scope of srlinux_controller.go. +var VariantsFS embed.FS // nolint:gochecknoglobals + +// SrlinuxReconciler reconciles a Srlinux object. type SrlinuxReconciler struct { client.Client Scheme *runtime.Scheme } -//+kubebuilder:rbac:groups=kne.srlinux.dev,resources=srlinuxes,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=kne.srlinux.dev,resources=srlinuxes/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=kne.srlinux.dev,resources=srlinuxes/finalizers,verbs=update -//+kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kne.srlinux.dev,resources=srlinuxes,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=kne.srlinux.dev,resources=srlinuxes/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=kne.srlinux.dev,resources=srlinuxes/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by +// Modify the Reconcile function to compare the state specified by // the Srlinux object against the actual cluster state, and then // perform operations to make the cluster state reflect the state specified by // the user. // // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.8.3/pkg/reconcile -func (r *SrlinuxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { +func (r *SrlinuxReconciler) Reconcile( + ctx context.Context, + req ctrl.Request, +) (ctrl.Result, error) { log := log.FromContext(ctx) + srlinux := &typesv1alpha1.Srlinux{} + + if res, isReturn, err := r.checkSrlinuxCR(ctx, log, req, srlinux); isReturn { + return res, err + } + + // Check if the srlinux pod already exists, if not create a new one + found := &corev1.Pod{} + + if res, isReturn, err := r.checkSrlinuxPod(ctx, log, srlinux, found); isReturn { + return res, err + } + + // updating Srlinux status + if res, isReturn, err := r.updateSrlinuxStatus(ctx, log, srlinux, found); isReturn { + return res, err + } + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *SrlinuxReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&typesv1alpha1.Srlinux{}). + Owns(&corev1.Pod{}). + Complete(r) +} - var srlinux = &typesv1alpha1.Srlinux{} - var err error +func (r *SrlinuxReconciler) checkSrlinuxCR( + ctx context.Context, + log logr.Logger, + req ctrl.Request, + srlinux *typesv1alpha1.Srlinux, +) (ctrl.Result, bool, error) { if err := r.Get(ctx, req.NamespacedName, srlinux); err != nil { if errors.IsNotFound(err) { // Request object not found, could have been deleted after reconcile request. // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. // Return and don't requeue - log.Info("Srlinux resource not found. Ignoring since object must be deleted") - return ctrl.Result{}, nil + log.Info("Srlinux resource not found. Ignoring since object must be deleted", + "NamespacedName", req.NamespacedName) + + return ctrl.Result{}, true, nil } // Error reading the object - requeue the request. log.Error(err, "Failed to get Srlinux") - return ctrl.Result{}, err + + return ctrl.Result{}, true, err } - // Check if the srlinux pod already exists, if not create a new one - found := &corev1.Pod{} - err = r.Get(ctx, types.NamespacedName{Name: srlinux.Name, Namespace: srlinux.Namespace}, found) + return ctrl.Result{}, false, nil +} + +func (r *SrlinuxReconciler) checkSrlinuxPod( + ctx context.Context, + log logr.Logger, + srlinux *typesv1alpha1.Srlinux, + found *corev1.Pod, +) (ctrl.Result, bool, error) { + err := r.Get(ctx, types.NamespacedName{Name: srlinux.Name, Namespace: srlinux.Namespace}, found) if err != nil && errors.IsNotFound(err) { - err = createConfigMapsIfNeeded(ctx, r, srlinux.Namespace, log) + err = createConfigMaps(ctx, r, srlinux.Namespace, log) if err != nil { - return ctrl.Result{}, err + return ctrl.Result{}, true, err } // Define a new srlinux pod pod := r.podForSrlinux(ctx, srlinux) log.Info("Creating a new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name) + err = r.Create(ctx, pod) if err != nil { - log.Error(err, "Failed to create new Pod", "Pod.Namespace", pod.Namespace, "Pod.Name", pod.Name) - return ctrl.Result{}, err + log.Error( + err, + "Failed to create new Pod", + "Pod.Namespace", + pod.Namespace, + "Pod.Name", + pod.Name, + ) + + return ctrl.Result{}, true, err } + // Pod created successfully - return and requeue - return ctrl.Result{Requeue: true}, nil + return ctrl.Result{Requeue: true}, true, nil } else if err != nil { log.Error(err, "Failed to get Pod") - return ctrl.Result{}, err - } - // updating Srlinux status - if !reflect.DeepEqual(found.Spec.Containers[0].Image, srlinux.Status.Image) { - log.Info("Updating srlinux image status to", "image", found.Spec.Containers[0].Image) - srlinux.Status.Image = found.Spec.Containers[0].Image - err = r.Status().Update(ctx, srlinux) - if err != nil { - log.Error(err, "Failed to update Srlinux status") - return ctrl.Result{}, err - } + return ctrl.Result{}, true, err } - return ctrl.Result{}, nil -} -// podForSrlinux returns a srlinux Pod object -func (r *SrlinuxReconciler) podForSrlinux(ctx context.Context, s *typesv1alpha1.Srlinux) *corev1.Pod { - log := log.FromContext(ctx) - - if s.Spec.Config.Env == nil { - s.Spec.Config.Env = map[string]string{} - } - s.Spec.Config.Env["SRLINUX"] = "1" // set default srlinux env var - - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: s.Name, - Namespace: s.Namespace, - Labels: map[string]string{ - "app": s.Name, - "topo": s.Namespace, - }, - }, - Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{{ - Name: fmt.Sprintf("init-%s", s.Name), - Image: InitContainerName, - Args: []string{ - fmt.Sprintf("%d", s.Spec.NumInterfaces+1), - fmt.Sprintf("%d", s.Spec.Config.Sleep), - }, - ImagePullPolicy: "IfNotPresent", - }}, - Containers: []corev1.Container{{ - Name: s.Name, - Image: s.Spec.GetImage(), - Command: s.Spec.Config.GetCommand(), - Args: s.Spec.Config.GetArgs(), - Env: knenode.ToEnvVar(s.Spec.Config.Env), - Resources: knenode.ToResourceRequirements(s.Spec.GetConstraints()), - ImagePullPolicy: "IfNotPresent", - SecurityContext: &corev1.SecurityContext{ - Privileged: pointer.Bool(true), - RunAsUser: pointer.Int64(0), - }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: variantsVolName, - MountPath: variantsVolMntPath, - }, - { - Name: topomacVolName, - MountPath: topomacVolMntPath, - }, - { - Name: entrypointVolName, - MountPath: entrypointVolMntPath, - SubPath: entrypointVolMntSubPath, - }, - }, - }}, - TerminationGracePeriodSeconds: pointer.Int64(0), - NodeSelector: map[string]string{}, - Affinity: &corev1.Affinity{ - PodAntiAffinity: &corev1.PodAntiAffinity{ - PreferredDuringSchedulingIgnoredDuringExecution: []corev1.WeightedPodAffinityTerm{{ - Weight: 100, - PodAffinityTerm: corev1.PodAffinityTerm{ - LabelSelector: &metav1.LabelSelector{ - MatchExpressions: []metav1.LabelSelectorRequirement{{ - Key: "topo", - Operator: "In", - Values: []string{s.Name}, - }}, - }, - TopologyKey: "kubernetes.io/hostname", - }, - }}, - }, - }, - Volumes: []corev1.Volume{ - { - Name: variantsVolName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: variantsCfgMapName, - }, - Items: []corev1.KeyToPath{ - { - Key: s.Spec.GetModel(), - Path: variantsTemplateTempName, - }, - }, - }, - }, - }, - { - Name: topomacVolName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: topomacCfgMapName, - }, - }, - }, - }, - { - Name: entrypointVolName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: entrypointCfgMapName, - }, - DefaultMode: pointer.Int32(0777), - }, - }, - }, - }, - }, - } - - // initialize config path and config file variables - cfgPath := defaultConfigPath - if p := s.Spec.GetConfig().ConfigPath; p != "" { - cfgPath = p - } - - cfgFile := s.Spec.GetConfig().ConfigFile - - // only create startup config mounts if the config data was set in kne - if s.Spec.Config.ConfigDataPresent { - log.Info("Adding volume for startup config to pod spec", "volume.name", "startup-config-volume", "mount.path", cfgPath+"/"+cfgFile) - pod.Spec.Volumes = append(pod.Spec.Volumes, corev1.Volume{ - Name: "startup-config-volume", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: fmt.Sprintf("%s-config", s.Name), - }, - }, - }, - }) - - pod.Spec.Containers[0].VolumeMounts = append(pod.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ - Name: "startup-config-volume", - MountPath: cfgPath + "/" + cfgFile, - SubPath: cfgFile, - ReadOnly: true, - }) - } - - _ = ctrl.SetControllerReference(s, pod, r.Scheme) - - return pod + return ctrl.Result{}, false, err } -// createConfigMapsIfNeeded creates srlinux-variants and srlinux-topomac config maps which every srlinux pod needs to mount -func createConfigMapsIfNeeded(ctx context.Context, r *SrlinuxReconciler, ns string, log logr.Logger) error { - // Check if the variants cfg map already exists, if not create a new one - cfgMap := &corev1.ConfigMap{} - err := r.Get(ctx, types.NamespacedName{Name: variantsCfgMapName, Namespace: ns}, cfgMap) - if err != nil && errors.IsNotFound(err) { - log.Info("Creating a new variants configmap") - data, err := VariantsFS.ReadFile("manifests/variants/srl_variants.yml") - if err != nil { - return err - } - decoder := serializer.NewCodecFactory(clientgoscheme.Scheme).UniversalDecoder() - err = runtime.DecodeInto(decoder, data, cfgMap) - if err != nil { - return err - } - cfgMap.ObjectMeta.Namespace = ns - err = r.Create(ctx, cfgMap) - if err != nil { - return err - } - } +func (r *SrlinuxReconciler) updateSrlinuxStatus( + ctx context.Context, + log logr.Logger, + srlinux *typesv1alpha1.Srlinux, + found *corev1.Pod, +) (ctrl.Result, bool, error) { + if !reflect.DeepEqual(found.Spec.Containers[0].Image, srlinux.Status.Image) { + log.Info("Updating srlinux image status to", "image", found.Spec.Containers[0].Image) + srlinux.Status.Image = found.Spec.Containers[0].Image - // Check if the topomac script cfg map already exists, if not create a new one - cfgMap = &corev1.ConfigMap{} - err = r.Get(ctx, types.NamespacedName{Name: topomacCfgMapName, Namespace: ns}, cfgMap) - if err != nil && errors.IsNotFound(err) { - log.Info("Creating a new topomac script configmap") - data, err := VariantsFS.ReadFile("manifests/variants/topomac.yml") + err := r.Status().Update(ctx, srlinux) if err != nil { - return err - } - decoder := serializer.NewCodecFactory(clientgoscheme.Scheme).UniversalDecoder() - err = runtime.DecodeInto(decoder, data, cfgMap) - if err != nil { - return err - } - cfgMap.ObjectMeta.Namespace = ns - err = r.Create(ctx, cfgMap) - if err != nil { - return err - } - } + log.Error(err, "Failed to update Srlinux status") - // Check if the kne-entrypoint cfg map already exists, if not create a new one - cfgMap = &corev1.ConfigMap{} - err = r.Get(ctx, types.NamespacedName{Name: entrypointCfgMapName, Namespace: ns}, cfgMap) - if err != nil && errors.IsNotFound(err) { - log.Info("Creating a new kne-entrypoint configmap") - data, err := VariantsFS.ReadFile("manifests/variants/kne-entrypoint.yml") - if err != nil { - return err - } - decoder := serializer.NewCodecFactory(clientgoscheme.Scheme).UniversalDecoder() - err = runtime.DecodeInto(decoder, data, cfgMap) - if err != nil { - return err - } - cfgMap.ObjectMeta.Namespace = ns - err = r.Create(ctx, cfgMap) - if err != nil { - return err + return ctrl.Result{}, true, err } } - return nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *SrlinuxReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&typesv1alpha1.Srlinux{}). - Owns(&corev1.Pod{}). - Complete(r) + return ctrl.Result{}, false, nil } diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 296b3a9..3025c25 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package controllers +package controllers_test import ( "path/filepath" @@ -36,8 +36,10 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. -var k8sClient client.Client -var testEnv *envtest.Environment +var ( + k8sClient client.Client // nolint: gochecknoglobals + testEnv *envtest.Environment // nolint: gochecknoglobals +) func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) @@ -68,7 +70,6 @@ var _ = BeforeSuite(func() { k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) - }, 60) var _ = AfterSuite(func() { diff --git a/main.go b/main.go index cf2dd23..2cf8b53 100644 --- a/main.go +++ b/main.go @@ -37,9 +37,11 @@ import ( //+kubebuilder:scaffold:imports ) +const ctrlManagerPort = 9443 + var ( - scheme = runtime.NewScheme() - setupLog = ctrl.Log.WithName("setup") + scheme = runtime.NewScheme() // nolint: gochecknoglobals + setupLog = ctrl.Log.WithName("setup") // nolint: gochecknoglobals //go:embed manifests/variants/* variantsFS embed.FS @@ -55,20 +57,37 @@ func init() { controllers.VariantsFS = variantsFS } -func main() { - +func main() { // nolint: funlen var metricsAddr string + var enableLeaderElection bool + var probeAddr string - flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") - flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + + flag.StringVar( + &metricsAddr, + "metrics-bind-address", + ":8080", + "The address the metric endpoint binds to.", + ) + + flag.StringVar( + &probeAddr, + "health-probe-bind-address", + ":8081", + "The address the probe endpoint binds to.", + ) + flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") + opts := zap.Options{ Development: true, } + opts.BindFlags(flag.CommandLine) + flag.Parse() ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) @@ -76,7 +95,7 @@ func main() { mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, MetricsBindAddress: metricsAddr, - Port: 9443, + Port: ctrlManagerPort, HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "8bce046c.srlinux.dev", @@ -99,12 +118,14 @@ func main() { setupLog.Error(err, "unable to set up health check") os.Exit(1) } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up ready check") os.Exit(1) } setupLog.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { setupLog.Error(err, "problem running manager") os.Exit(1)