diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a01faf8 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,98 @@ +name: Linting +on: + pull_request: + +jobs: + golangci: + name: Lint golang files + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha }} + repository: ${{github.event.pull_request.head.repo.full_name}} + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: false + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6.1.1 + with: + only-new-issues: true + version: v1.62.2 + args: --timeout=900s + + gomodtidy: + name: Enforce go.mod tidiness + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: "${{ github.event.pull_request.head.sha }}" + repository: ${{github.event.pull_request.head.repo.full_name}} + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + + - name: Execute go mod tidy and check the outcome + working-directory: ./ + run: | + go mod tidy + exit_code=$(git diff --exit-code) + exit ${exit_code} + + - name: Issue a comment in case the of failure + uses: peter-evans/create-or-update-comment@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + issue-number: ${{ github.event.pull_request.number }} + body: | + The `go.mod` and/or `go.sum` files appear not to be correctly tidied. + + Please, rerun `go mod tidy` to fix the issues. + reactions: confused + if: | + failure() && github.event.pull_request.head.repo.full_name == github.repository + + shelllint: + name: Lint bash files + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: "${{ github.event.pull_request.head.sha }}" + repository: ${{github.event.pull_request.head.repo.full_name}} + persist-credentials: false + - name: Run Shellcheck + uses: ludeeus/action-shellcheck@2.0.0 + env: + # Allow 'source' outside of FILES + SHELLCHECK_OPTS: -x + + markdownlint: + name: Lint markdown files + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + ref: "${{ github.event.pull_request.head.sha }}" + repository: ${{github.event.pull_request.head.repo.full_name}} + persist-credentials: false + - name: Lint markdown files + uses: avto-dev/markdown-lint@v1 + with: + config: '.markdownlint.yml' + args: '**/*.md' + ignore: .github/ISSUE_TEMPLATE/*.md diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..e0de61b --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,140 @@ +run: + timeout: 10m + +linters-settings: + exhaustive: + check-generated: false + default-signifies-exhaustive: true + + lll: + line-length: 150 + gomodguard: + blocked: + modules: + - github.com/go-logr/logr: + recommendations: + - k8s.io/klog/v2 + gci: + sections: + - standard # Captures all standard packages if they do not match another section. + - default # Contains all imports that could not be matched to another section type. + - prefix(github.com/liqotech/resource-slice-class-controller-template) # Groups all imports with the specified Prefix. + goconst: + min-len: 2 + min-occurrences: 2 + gocritic: + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + disabled-checks: + # Conflicts with govet check-shadowing + - sloppyReassign + goimports: + local-prefixes: github.com/liqotech/resource-slice-class-controller-template + govet: + enable: + - shadow + - nilness + - nilfunc + misspell: + locale: US + nolintlint: + allow-unused: false # report any unused nolint directives + require-explanation: true # require an explanation for nolint directives + require-specific: true # require nolint directives to be specific about which linter is being skipped + dupl: + threshold: 300 + +linters: + disable-all: true + enable: + - asciicheck + - bodyclose + - copyloopvar + # - depguard + - dogsled + - dupl + - errcheck + - errorlint + - exhaustive + # - funlen + # - gochecknoglobals + # - gochecknoinits + # - gocognit + - gci + - goconst + - gocritic + - gocyclo + - godot + # - godox + # - goerr113 + - gofmt + - goheader + - goimports + - gomodguard + # - gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - lll + # - maligned + - misspell + - nakedret + # - nestif + - noctx + - nolintlint + # - prealloc + - revive + - rowserrcheck + - staticcheck + - stylecheck + # - testpackage + - typecheck + - unconvert + - unparam + - unused + - whitespace + # - wsl + +issues: + #fix: true + + max-issues-per-linter: 0 + max-same-issues: 0 + + # Disable the default exclude patterns (as they disable the mandatory comments) + exclude-use-default: false + exclude: + # errcheck: Almost all programs ignore errors on these functions and in most cases it's ok + - Error return value of .((os\.)?std(out|err)\..*|.*Close|.*Flush|os\.Remove(All)?|.*printf?|os\.(Un)?Setenv). is not checked + + exclude-rules: + - linters: + - govet + text: 'declaration of "(err|ctx)" shadows declaration at' + - linters: + - gosec + # Disable the check to test that HTTP clients are not using an insecure TLS connection. + # We need it to contact the remote authentication services exposing a self-signed certificate + text: TLS InsecureSkipVerify set true. + - linters: + - errorlint + # Disable the check to test errors type assertion on switches. + text: type switch on error will fail on wrapped errors. Use errors.As to check for specific errors + + # Exclude the following linters from running on tests files. + - path: _test\.go + linters: + - whitespace + - revive # it does not allow dot imports (e.g. . "github.com/onsi/ginkgo") + + exclude-files: + - "zz_generated.*.go" + + exclude-dirs: + - "pkg/client" diff --git a/.markdownlint.yml b/.markdownlint.yml new file mode 100644 index 0000000..bbc10ab --- /dev/null +++ b/.markdownlint.yml @@ -0,0 +1,5 @@ +default: true +line-length: false +no-inline-html: false +no-duplicate-header: + siblings_only: true diff --git a/README.md b/README.md index 4e3f2c7..9b02ddc 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,14 @@ The controller manages the ResourceSlice status updates and conditions, while th ## Installation 1. Clone the repository: + ```bash git clone https://github.com/liqotech/resource-slice-class-controller-template.git cd resource-slice-class-controller-template ``` 2. Build the controller: + ```bash go build -o bin/manager main.go ``` @@ -40,6 +42,7 @@ The controller requires a class name to be specified: ``` Additional flags: + - `--metrics-bind-address`: The address to bind the metrics endpoint (default: ":8080") - `--health-probe-bind-address`: The address to bind the health probe endpoint (default: ":8081") - `--leader-elect`: Enable leader election for controller manager @@ -47,6 +50,7 @@ Additional flags: ### Example Implementation The repository includes an example handler implementation in `example/resourceslice/handler.go` that: + - Generates CPU resources between 1 and 10 cores - Generates Memory resources between 1 and 5 GB - Allocates 110 pods @@ -58,56 +62,56 @@ To implement a custom handler for your ResourceSlice class: 1. Create a new type that implements the `handler.Handler` interface: -```go -package myhandler + ```go + package myhandler -import ( - "context" - authv1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1" - "github.com/liqotech/resource-slice-classes/pkg/resourceslice/handler" - ctrl "sigs.k8s.io/controller-runtime" -) + import ( + "context" + authv1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1" + rshandler "github.com/liqotech/resource-slice-class-controller-template/pkg/resourceslice/handler" + ctrl "sigs.k8s.io/controller-runtime" + ) -type MyHandler struct {} + type MyHandler struct {} -func NewMyHandler() handler.Handler { - return &MyHandler{} -} + func NewMyHandler() rshandler.Handler { + return &MyHandler{} + } -func (h *MyHandler) Handle(ctx context.Context, resourceSlice *authv1beta1.ResourceSlice) (ctrl.Result, error) { - // Implement your custom resource allocation logic here - // Update resourceSlice.Status.Resources with your allocated resources - - return ctrl.Result{}, nil -} -``` + func (h *MyHandler) Handle(ctx context.Context, resourceSlice *authv1beta1.ResourceSlice) (ctrl.Result, error) { + // Implement your custom resource allocation logic here + // Update resourceSlice.Status.Resources with your allocated resources + + return ctrl.Result{}, nil + } + ``` 2. Update `main.go` to use your custom handler: -```go -import ( - "github.com/your-org/your-module/pkg/myhandler" -) - -func main() { - // ... - - // Create your custom handler - customHandler := myhandler.NewMyHandler() - - if err = controller.NewResourceSliceReconciler( - mgr.GetClient(), - mgr.GetScheme(), - mgr.GetEventRecorderFor("resource-slice-controller"), - className, - customHandler, - ).SetupWithManager(mgr); err != nil { + ```go + import ( + "github.com/your-org/your-module/pkg/myhandler" + ) + + func main() { + // ... + + // Create your custom handler + customHandler := myhandler.NewMyHandler() + + if err = controller.NewResourceSliceReconciler( + mgr.GetClient(), + mgr.GetScheme(), + mgr.GetEventRecorderFor("resource-slice-controller"), + className, + customHandler, + ).SetupWithManager(mgr); err != nil { + // ... + } + // ... } - - // ... -} -``` + ``` ## Handler Interface @@ -120,11 +124,13 @@ type Handler interface { ``` Your handler implementation should: + 1. Implement your resource allocation strategy 2. Set the allocated resources in `resourceSlice.Status.Resources` 3. Return appropriate reconciliation results and errors Note: The controller, not the handler, is responsible for: + - Updating the ResourceSlice status in the API server - Managing ResourceSlice conditions - Recording events @@ -150,7 +156,3 @@ Note: The controller, not the handler, is responsible for: ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. - -## License - -This project is licensed under the Apache License 2.0 - see the LICENSE file for details. diff --git a/example/resourceslice/handler.go b/example/resourceslice/handler.go index d4e2e4a..fbe9d81 100644 --- a/example/resourceslice/handler.go +++ b/example/resourceslice/handler.go @@ -1,3 +1,4 @@ +// Package resourceslice contains an example handler for ResourceSlice. package resourceslice import ( @@ -5,25 +6,30 @@ import ( "hash/fnv" authv1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1" - "github.com/liqotech/resource-slice-classes/pkg/resourceslice/handler" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" + + rshandler "github.com/liqotech/resource-slice-class-controller-template/pkg/resourceslice/handler" ) -// ResourceSliceHandler implements the Handler interface for ResourceSlice -type ResourceSliceHandler struct {} +// Handler implements the Handler interface for ResourceSlice. +type Handler struct{} -// NewResourceSliceHandler creates a new ResourceSliceHandler -func NewResourceSliceHandler() handler.Handler { - return &ResourceSliceHandler{} +// NewHandler creates a new ResourceSliceHandler. +func NewHandler() rshandler.Handler { + return &Handler{} } -// Handle processes a ResourceSlice -func (h *ResourceSliceHandler) Handle(ctx context.Context, resourceSlice *authv1beta1.ResourceSlice) (ctrl.Result, error) { +// Handle processes a ResourceSlice. +func (h *Handler) Handle(_ context.Context, resourceSlice *authv1beta1.ResourceSlice) (ctrl.Result, error) { // Generate and update resources in status - resources := h.generateResourcesFromName(resourceSlice.Name) + resources, err := h.generateResourcesFromName(resourceSlice.Name) + if err != nil { + return ctrl.Result{}, err + } + resourceSlice.Status.Resources = resources klog.V(4).InfoS("Updated ResourceSlice status", @@ -36,11 +42,13 @@ func (h *ResourceSliceHandler) Handle(ctx context.Context, resourceSlice *authv1 return ctrl.Result{}, nil } -// generateResourcesFromName generates resource quantities based on the ResourceSlice name -func (h *ResourceSliceHandler) generateResourcesFromName(name string) corev1.ResourceList { +// generateResourcesFromName generates resource quantities based on the ResourceSlice name. +func (h *Handler) generateResourcesFromName(name string) (corev1.ResourceList, error) { // Create a hash of the name hash := fnv.New32a() - hash.Write([]byte(name)) + if _, err := hash.Write([]byte(name)); err != nil { + return nil, err + } hashVal := hash.Sum32() // Use the hash to generate resource quantities (between 1 and 10) @@ -51,5 +59,5 @@ func (h *ResourceSliceHandler) generateResourcesFromName(name string) corev1.Res corev1.ResourceCPU: *resource.NewQuantity(int64(cpuCount), resource.DecimalSI), corev1.ResourceMemory: *resource.NewQuantity(int64(memoryGB*1024*1024*1024), resource.BinarySI), corev1.ResourcePods: *resource.NewQuantity(110, resource.DecimalSI), - } + }, nil } diff --git a/go.mod b/go.mod index 637a0de..8c9378b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/liqotech/resource-slice-classes +module github.com/liqotech/resource-slice-class-controller-template go 1.23.3 diff --git a/main.go b/main.go index 0510896..241db62 100644 --- a/main.go +++ b/main.go @@ -1,21 +1,21 @@ +// Package main contains an an example main to setup and run a controller handling a ResourceSlice class package main import ( "flag" "os" - auth1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1" + authv1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" - _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" - examplehandler "github.com/liqotech/resource-slice-classes/example/resourceslice" - "github.com/liqotech/resource-slice-classes/pkg/controller" + examplehandler "github.com/liqotech/resource-slice-class-controller-template/example/resourceslice" + "github.com/liqotech/resource-slice-class-controller-template/pkg/controller" ) var ( @@ -24,7 +24,7 @@ var ( func init() { utilruntime.Must(clientgoscheme.AddToScheme(scheme)) - utilruntime.Must(auth1beta1.AddToScheme(scheme)) + utilruntime.Must(authv1beta1.AddToScheme(scheme)) // Add custom resource scheme here when you have CRDs //+kubebuilder:scaffold:scheme } @@ -46,7 +46,7 @@ func main() { flag.Parse() if className == "" { - klog.ErrorS(nil, "class-name is required") + klog.Error("class-name is required") os.Exit(1) } @@ -61,12 +61,12 @@ func main() { LeaderElectionID: "resource-slice-classes-leader-election", }) if err != nil { - klog.ErrorS(err, "unable to start manager") + klog.Errorf("unable to start manager: %v", err) os.Exit(1) } // Create the handler - rsHandler := examplehandler.NewResourceSliceHandler() + rsHandler := examplehandler.NewHandler() if err = controller.NewResourceSliceReconciler( mgr.GetClient(), @@ -75,22 +75,22 @@ func main() { className, rsHandler, ).SetupWithManager(mgr); err != nil { - klog.ErrorS(err, "unable to create controller", "controller", "ResourceSlice") + klog.Errorf("unable to setup controller: %v", err) os.Exit(1) } if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { - klog.ErrorS(err, "unable to set up health check") + klog.Errorf("unable to set up health check: %v", err) os.Exit(1) } if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { - klog.ErrorS(err, "unable to set up ready check") + klog.Errorf("unable to set up ready check: %v", err) os.Exit(1) } klog.Info("starting manager") if err := mgr.Start(ctx); err != nil { - klog.ErrorS(err, "problem running manager") + klog.Errorf("unable to start controller: %v", err) os.Exit(1) } } diff --git a/pkg/controller/resource_slice_controller.go b/pkg/controller/resource_slice_controller.go index a55a20b..cf4fdd9 100644 --- a/pkg/controller/resource_slice_controller.go +++ b/pkg/controller/resource_slice_controller.go @@ -1,3 +1,4 @@ +// Package controller contains the ResourceSlice reconciler. package controller import ( @@ -6,13 +7,14 @@ import ( authv1beta1 "github.com/liqotech/liqo/apis/authentication/v1beta1" "github.com/liqotech/liqo/pkg/liqo-controller-manager/authentication" - "github.com/liqotech/resource-slice-classes/pkg/resourceslice/handler" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/predicate" + + rshandler "github.com/liqotech/resource-slice-class-controller-template/pkg/resourceslice/handler" ) // ResourceSliceReconciler reconciles a ResourceSlice object. @@ -21,13 +23,14 @@ type ResourceSliceReconciler struct { Scheme *runtime.Scheme recorder record.EventRecorder className string - handler handler.Handler + handler rshandler.Handler } // NewResourceSliceReconciler creates a new ResourceSliceReconciler. -func NewResourceSliceReconciler(client client.Client, scheme *runtime.Scheme, recorder record.EventRecorder, className string, handler handler.Handler) *ResourceSliceReconciler { +func NewResourceSliceReconciler(cl client.Client, scheme *runtime.Scheme, recorder record.EventRecorder, + className string, handler rshandler.Handler) *ResourceSliceReconciler { return &ResourceSliceReconciler{ - Client: client, + Client: cl, Scheme: scheme, recorder: recorder, className: className, @@ -51,19 +54,17 @@ func (r *ResourceSliceReconciler) SetupWithManager(mgr ctrl.Manager) error { // Reconcile handles the reconciliation loop for ResourceSlice resources. func (r *ResourceSliceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) { - klog.V(4).InfoS("Reconciling ResourceSlice", "name", req.Name, "namespace", req.Namespace, "className", r.className) + klog.V(4).Infof("Reconciling ResourceSlice %q (class: %q)", req.NamespacedName, r.className) // Fetch the ResourceSlice instance - resourceSlice := authv1beta1.ResourceSlice{} + var resourceSlice authv1beta1.ResourceSlice if err = r.Get(ctx, req.NamespacedName, &resourceSlice); err != nil { return ctrl.Result{}, client.IgnoreNotFound(err) } // Wait for the Authentication condition to be ready - if resourceSlice.Status.Conditions == nil { - return ctrl.Result{}, nil - } - if authentication.GetCondition(&resourceSlice, authv1beta1.ResourceSliceConditionTypeAuthentication).Status != authv1beta1.ResourceSliceConditionAccepted { + authCond := authentication.GetCondition(&resourceSlice, authv1beta1.ResourceSliceConditionTypeAuthentication) + if authCond == nil || authCond.Status != authv1beta1.ResourceSliceConditionAccepted { return ctrl.Result{}, nil } @@ -76,9 +77,12 @@ func (r *ResourceSliceReconciler) Reconcile(ctx context.Context, req ctrl.Reques defer func() { // Update the status - if err = r.Status().Update(ctx, &resourceSlice); err != nil { + if newErr := r.Status().Update(ctx, &resourceSlice); newErr != nil { + if err != nil { + klog.Error(err) + } r.recorder.Eventf(&resourceSlice, "Warning", "Failed", "Failed to update ResourceSlice status: %v", err) - err = fmt.Errorf("failed to update ResourceSlice status: %w", err) + err = fmt.Errorf("failed to update ResourceSlice status: %w", newErr) } }() @@ -93,6 +97,7 @@ func (r *ResourceSliceReconciler) Reconcile(ctx context.Context, req ctrl.Reques "ResourceSliceResourcesAccepted", "ResourceSlice resources accepted", ) + // Return the reconciliation result return res, nil } diff --git a/pkg/resourceslice/handler/interface.go b/pkg/resourceslice/handler/interface.go index 1b80ad8..51367cc 100644 --- a/pkg/resourceslice/handler/interface.go +++ b/pkg/resourceslice/handler/interface.go @@ -1,3 +1,4 @@ +// Package handler contains the interface for an handler that manages ResourceSlices resources. package handler import ( @@ -7,7 +8,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" ) -// Handler defines the interface for handling ResourceSlice operations +// Handler defines the interface for handling ResourceSlice operations. type Handler interface { // Handle processes a ResourceSlice and returns a reconciliation result Handle(ctx context.Context, resourceSlice *authv1beta1.ResourceSlice) (ctrl.Result, error)