From 3254073000ad74deadfd5ab5739b9d44db177821 Mon Sep 17 00:00:00 2001 From: Daniel Roux Date: Mon, 1 Apr 2024 20:11:54 +0200 Subject: [PATCH] Setup checks related to golang in gha/ci && Address issues - Add .golangci.yml --- .changelog/19.txt | 3 + .github/workflows/golang.yml | 90 ++++ .golangci.yml | 793 +++++++++++++++++++++++++++++++++++ dashboards.go | 52 ++- k8s_api.go | 29 +- main.go | 44 +- metrics.go | 8 +- sk8l.go | 225 +++++++--- tls.go | 78 +++- tls_test.go | 128 ++++++ version.go | 2 +- 11 files changed, 1312 insertions(+), 140 deletions(-) create mode 100644 .changelog/19.txt create mode 100644 .github/workflows/golang.yml create mode 100644 .golangci.yml create mode 100644 tls_test.go diff --git a/.changelog/19.txt b/.changelog/19.txt new file mode 100644 index 0000000..0dd0354 --- /dev/null +++ b/.changelog/19.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +golang: Setup checks related to golang in gha/ci +``` diff --git a/.github/workflows/golang.yml b/.github/workflows/golang.yml new file mode 100644 index 0000000..263d51f --- /dev/null +++ b/.github/workflows/golang.yml @@ -0,0 +1,90 @@ +name: Go + +on: + push: + branches: + - main + pull_request: + +# permissions: +# # Required: allow read access to the content for analysis. +# contents: read +# # Optional: allow read access to pull request. Use with `only-new-issues` option. +# # pull-requests: read +# # Optional: Allow write access to checks to allow the action to annotate code in the PR. +# checks: write + +jobs: + linters: + runs-on: ubuntu-latest + strategy: + matrix: + go: [ '^1.22' ] + name: Go ${{ matrix.go }} linters + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: ${{ matrix.go }} + check-latest: false + cache: false + id: go + - name: golangci-lint + uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0 + with: + version: v1.57 + # Optional: golangci-lint command line arguments. + # + # Note: By default, the `.golangci.yml` file should be at the root of the repository. + # The location of the configuration file can be changed by using `--config=` + # args: --timeout=30m --config=/my/path/.golangci.yml --issues-exit-code=0 + + # Optional: show only new issues if it's a pull request. The default value is `false`. + # only-new-issues: true + + # skip-cache: true + tests: + runs-on: ubuntu-latest + strategy: + matrix: + go: [ '^1.22' ] + name: Go ${{ matrix.go }} tests + # needs: [linters] + env: + CA_CERT: ${{ secrets.CA_CERT }} + CA_KEY: ${{ secrets.CA_KEY }} + INVALID_CERT: ${{ secrets.INVALID_CERT }} + INVALID_CERT_KEY: ${{ secrets.INVALID_CERT_KEY }} + SERVER_CERT: ${{ secrets.SERVER_CERT }} + SERVER_CERT_KEY: ${{ secrets.SERVER_CERT_KEY }} + SERVER_REQ_PEM: ${{ secrets.SERVER_REQ_PEM }} + steps: + - name: Checkout + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + with: + fetch-depth: 0 + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491 # v5.0.0 + with: + go-version: ${{ matrix.go }} + check-latest: false + cache: false + id: go + - name: Get dependencies + run: go get -v -t -d ./... + - name: generate certs for tests + run: | + mkdir -p testdata + echo "$CA_CERT" > testdata/ca-cert.pem + echo "$CA_KEY" > testdata/ca-key.pem + echo "$INVALID_CERT" > testdata/invalid-cert.pem + echo "$INVALID_CERT_KEY" > testdata/invalid-cert.key + echo "$SERVER_CERT" > testdata/server-cert.pem + echo "$SERVER_CERT_KEY" > testdata/server-key.key + echo "$SERVER_REQ_PEM" > testdata/server-req.pem + - name: Test + run: go test -v -coverprofile=coverage.out ./... diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..ad23ac4 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,793 @@ +# This configuration file is used with different version of golangci-lint to avoid regressions: +# the linters can change between version, their configuration can change as well. + +run: + # default concurrency is a available CPU number + concurrency: 4 + + # timeout for analysis, e.g. 30s, 5m, default is 1m + timeout: 5m + + # exit code when at least one issue was found, default is 1 + issues-exit-code: 1 + + # include test files or not, default is true + tests: true + + # which dirs to skip: they won't be analyzed; + # can use regexp here: generated.*, regexp is applied on full path + exclude-dirs: + - test/testdata_etc + - tooling + - vendor + - third_party + + # # which files to skip: they will be analyzed, but issues from them won't be reported; + # # can use regexp here: generated.*, regexp is applied on full path + exclude-files: + # - ".*\.my\.go$" + - lib/bad.go + +linters: + # please, do not use `enable-all`: it is deprecated and will be removed soon. + # Run `golangci-lint run --help` to see all available linters. + enable: + - bodyclose + - depguard + - dogsled + - dupl + - errcheck + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - godot + - godox + - goerr113 + - gofmt + - goimports + - gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - lll + - misspell + - nakedret + - nolintlint + - revive + - rowserrcheck + - exportloopref + - staticcheck + - stylecheck + - typecheck + - unconvert + # - unparam + - unused + - whitespace + +linters-settings: + #staticcheck: + + # # SAxxxx checks in https://staticcheck.io/docs/configuration/options/#checks + # # Default: ["*"] + # checks: + # - "all" + + # Specify which checks to disable. + # This overrides the "checks" setting above. + # See https://staticcheck.io/docs/configuration/options/#checks for available checks. + # disabled-checks: + # - "SA1019" + # - "ST1003" + + # Specify the severity level for each check. + # Available severity levels: "error", "warning", "info", "none". + # See https://staticcheck.io/docs/configuration/options/#severity for more details. + # severities: + # "ST1000": "warning" # Treat the "ST1000" check as a warning instead of an error + # "ST1003": "none" # Completely disable the "ST1003" check + + # Specify a list of packages to ignore. + # This is useful for ignoring external dependencies or generated code. + # packages: + # - "github.com/example/external-package" + # - "example.com/generated/code" + + # Specify a list of files to ignore. + # This is useful for ignoring specific files or directories. + # files: + # - "internal/generated/code.go" + # - "vendor/**/*.go" + + # Specify a list of functions to ignore. + # This is useful for ignoring specific functions that may trigger false positives. + # functions: + # - "github.com/example/package.IgnoredFunction" + # - "example.com/package.AnotherIgnoredFunction" + + #cyclop: + # # The maximal code complexity to report. + # # Default: 10 + # max-complexity: 30 + # # The maximal average package complexity. + # # If it's higher than 0.0 (float) the check is enabled + # # Default: 0.0 + # package-average: 10.0 + + dupl: + # tokens count to trigger issue, 150 by default + threshold: 100 + + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: false + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(x)`; + # default is false: such cases aren't reported by default. + check-blank: false + + #exhaustive: + # # Program elements to check for exhaustiveness. + # # Default: [ switch ] + # check: + # - switch + # - map + + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 210 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 55 + # Ignore comments when counting lines. + # Default false + ignore-comments: true + + gomnd: + # List of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. + # Default: ["argument", "case", "condition", "operation", "return", "assign"] + checks: + - argument + - case + - condition + - operation + - return + - assign + + # ignored-functions: [] + ignored-files: + - 'main.go' + - 'dashboards.go' + - 'sk8l.go' + - 'metrics.go' + + # gomodguard: + # blocked: + # # List of blocked modules. + # # Default: [] + + gosec: + # To select a subset of rules to run. + # Available rules: https://github.com/securego/gosec#available-rules + # Default: [] - means include all rules + includes: [] + + govet: + enable-all: true + disable: + - shadow + # settings: + # shadow: + # # Whether to be strict about shadowing; can be noisy. + # # Default: false + # strict: false + + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 60 + + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + + depguard: + rules: + # Name of a rule. + main: + # Used to determine the package matching priority. + # There are three different modes: `original`, `strict`, and `lax`. + # Default: "original" + list-mode: lax + # Packages that are not allowed where the value is a suggestion. + deny: + - pkg: "github.com/sirupsen/logrus" + desc: not allowed + - pkg: "github.com/pkg/errors" + desc: Should be replaced by standard lib errors package + + # depguard: + # list-type: whitelist + # packages: + # - github.com/danroux/sk8l/protos + # - github.com/prometheus/client_golang/prometheu + # - k8s.io/apimachinery/pkg/types + # - k8s.io/api/batch/v1 + # - k8s.io/api/core/v1 + # - github.com/dgraph-io/badger/v4 + + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + + lll: + # max line length, lines longer will be reported. Default is 120. + # ' ' is counted as 1 character by default, and can be changed with the tab-width option. + line-length: 150 + # tab width in spaces. Default to 1. + tab-width: 1 + + unparam: + # Inspect exported functions, default is false. Set to true if no external program/library imports your code. + # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: + # if it can't find external functions, it will think they are unused. + check-exported: false + + revive: + # Maximum number of open files at the same time. + # See https://github.com/mgechev/revive#command-line-flags + # Defaults to unlimited. + max-open-files: 2048 + # When set to false, ignores files with "GENERATED" header, similar to golint. + # See https://github.com/mgechev/revive#available-rules for details. + # Default: false + ignore-generated-header: true + # Sets the default severity. + # See https://github.com/mgechev/revive#configuration + # Default: warning + severity: warning + # Enable all available rules. + # Default: false + enable-all-rules: true + # Sets the default failure confidence. + # This means that linting errors with less than 0.8 confidence will be ignored. + # Default: 0.8 + confidence: 0.8 + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant + - name: add-constant + severity: error + disabled: true + # exclude: [""] + # arguments: + # - maxLitCount: "3" + # allowStrs: '""' + # allowInts: "0,1,2" + # allowFloats: "0.0,0.,1.0,1.,2.0,2." + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#argument-limit + - name: argument-limit + severity: warning + disabled: false + exclude: [""] + # arguments: [4] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic + - name: atomic + severity: warning + exclude: [""] + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#banned-characters + - name: banned-characters + severity: warning + disabled: true + exclude: [""] + # arguments: ["Ω", "Σ", "σ", "7"] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bare-return + - name: bare-return + severity: warning + exclude: [""] + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#blank-imports + - name: blank-imports + severity: warning + exclude: [""] + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr + - name: bool-literal-in-expr + severity: warning + exclude: [""] + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#call-to-gc + - name: call-to-gc + severity: warning + exclude: [""] + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity + - name: cognitive-complexity + severity: warning + disabled: false + exclude: [""] + arguments: [60] # 7 + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#comment-spacings + # - name: comment-spacings + # severity: warning + # disabled: false + # exclude: [""] + # arguments: + # - mypragma + # - otherpragma + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-naming + - name: confusing-naming + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-results + - name: confusing-results + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#constant-logical-expr + - name: constant-logical-expr + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument + - name: context-as-argument + severity: warning + disabled: false + exclude: [""] + # arguments: + # - allowTypesBefore: "*testing.T,*github.com/user/repo/testing.Harness" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-keys-type + - name: context-keys-type + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic + - name: cyclomatic + severity: warning + disabled: false + exclude: [""] + arguments: [20] # 10 + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#datarace + - name: datarace + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit + - name: deep-exit + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#defer + - name: defer + severity: warning + disabled: false + exclude: [""] + # arguments: + # - ["call-chain", "loop"] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#dot-imports + - name: dot-imports + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#duplicated-imports + - name: duplicated-imports + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return + - name: early-return + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "preserveScope" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block + - name: empty-block + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines + - name: empty-lines + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#enforce-map-style + - name: enforce-map-style + severity: warning + disabled: false + exclude: [""] + arguments: + - "any" + # - "make" + # - "literal" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#enforce-repeated-arg-type-style + - name: enforce-repeated-arg-type-style + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "short" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#enforce-slice-style + - name: enforce-slice-style + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "make" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-naming + - name: error-naming + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-return + - name: error-return + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-strings + - name: error-strings + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#errorf + - name: errorf + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported + - name: exported + severity: warning + disabled: true + exclude: [""] + # arguments: + # - "preserveScope" + # - "checkPrivateReceivers" + # - "sayRepetitiveInsteadOfStutters" + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#file-header + # - name: file-header + # severity: warning + # disabled: false + # exclude: [""] + # arguments: + # - This is the text that must appear at the top of source files. + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter + - name: flag-parameter + severity: warning + disabled: true + exclude: [""] + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-result-limit + - name: function-result-limit + severity: warning + disabled: false + exclude: [""] + arguments: [4] + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-length + - name: function-length + severity: warning + disabled: false + exclude: [""] + arguments: [55, 210] # (int,int) the maximum allowed statements and lines + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#get-return + - name: get-return + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#identical-branches + - name: identical-branches + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return + - name: if-return + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#increment-decrement + - name: increment-decrement + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#indent-error-flow + - name: indent-error-flow + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "preserveScope" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-alias-naming + - name: import-alias-naming + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "^[a-z][a-z0-9]{0,}$" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blocklist + - name: imports-blocklist + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "crypto/md5" + # - "crypto/sha1" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing + - name: import-shadowing + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit + - name: line-length-limit + severity: error + disabled: false + exclude: [""] + arguments: [150] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-control-nesting + - name: max-control-nesting + severity: warning + disabled: false + exclude: [""] + arguments: [4] + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-public-structs + - name: max-public-structs + severity: warning + disabled: true + exclude: [""] + arguments: [3] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-parameter + - name: modifies-parameter + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-value-receiver + - name: modifies-value-receiver + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs + - name: nested-structs + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#optimize-operands-order + - name: optimize-operands-order + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments + - name: package-comments + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range + - name: range + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-in-closure + - name: range-val-in-closure + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-address + - name: range-val-address + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming + - name: receiver-naming + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redundant-import-alias + - name: redundant-import-alias + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redefines-builtin-id + - name: redefines-builtin-id + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-of-int + - name: string-of-int + severity: warning + disabled: false + exclude: [""] + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format + # - name: string-format + # severity: warning + # disabled: false + # exclude: [""] + # arguments: + # - - 'core.WriteError[1].Message' + # - '/^([^A-Z]|$)/' + # - must not start with a capital letter + # - - 'fmt.Errorf[0]' + # - '/(^|[^\.!?])$/' + # - must not end in punctuation + # - - panic + # - '/^[^\n]*$/' + # - must not contain line breaks + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag + # - name: struct-tag + # arguments: + # - "json,inline" + # - "bson,outline,gnu" + # severity: warning + # disabled: false + # exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#superfluous-else + - name: superfluous-else + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "preserveScope" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-equal + - name: time-equal + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-naming + - name: time-naming + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-naming + - name: var-naming + severity: warning + disabled: false + exclude: [""] + # arguments: + # - ["ID"] # AllowList + # - ["VM"] # DenyList + # - - upperCaseConst: true + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-declaration + - name: var-declaration + severity: warning + disabled: false + # exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unconditional-recursion + - name: unconditional-recursion + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming + - name: unexported-naming + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-return + - name: unexported-return + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error + - name: unhandled-error + severity: warning + disabled: false + exclude: [""] + # arguments: + # - "fmt.Printf" + # - "myFunction" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unnecessary-stmt + - name: unnecessary-stmt + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code + - name: unreachable-code + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter + - name: unused-parameter + severity: warning + disabled: true + exclude: [""] + # arguments: + # - allowRegex: "^_" + # # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver + - name: unused-receiver + severity: error + disabled: true + exclude: [""] + # arguments: + # - allowRegex: "^_" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break + - name: useless-break + severity: warning + disabled: false + exclude: [""] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#waitgroup-by-value + - name: waitgroup-by-value + severity: warning + disabled: false + exclude: [""] + + unused: + # treat code as a program (not a library) and report unused exported identifiers; default is false. + check-exported: false + + nakedret: + # make an issue when you have 3 or more consecutive return statements without parenthesis. Default is maxed at 1. + max-func-lines: 1 + + prealloc: + # XXX: we don't recommend using this linter before doing performance profiling. + # For most programs usage of prealloc will be a premature optimization. + + # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. + # True by default. + simple: true + + # Report preallocation suggestions on range loops, but only if the range expression is constant. + # True by default. + range-loops: true + + # Report preallocation suggestions on for loops, but only for loops that have no returns/breaks/continues/gotos in them. + # False by default. + for-loops: false + +issues: + # List of regexps of issue texts to exclude, empty list by default. + # But independently from this option we use default exclude patterns, + # it can be disabled by `exclude-use-default: false`. To list all + # excluded by default patterns execute `golangci-lint run --help` + exclude: + - abcdef + + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # Exclude some linters from running on tests files. + - path: _test\.go + linters: + - gocyclo + - errcheck + - dupl + - gosec + + # Exclude known linters from partially hard-vendored code, + # these patterns will apply to all components. + - path: vendor/github.com/stretchr/testify/ + linters: + - gosec + + # Exclude some staticcheck messages + - linters: + - staticcheck + text: "SA9003" + + # Exclude lll issues for long lines with go:generate + # - linters: + # - lll + # # source: "^//\s*go:generate" + + # Independently from option `exclude` we use default exclude patterns, + # it can be disabled by this option. To list all + # excluded by default patterns execute `golangci-lint run --help`. + # Default value for this option is true. + exclude-use-default: false + + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + +output: + show-stats: true diff --git a/dashboards.go b/dashboards.go index bf3fdb5..be2ed07 100644 --- a/dashboards.go +++ b/dashboards.go @@ -1,17 +1,19 @@ +// Sk8l package main import ( "fmt" + "log" "regexp" ) type DataSource struct { - Type, Uid string + UID, Type string } type Target struct { - Expr, LegendFormat string DataSource *DataSource + Expr, LegendFormat string } type GridPos struct { @@ -26,24 +28,24 @@ type Option struct { } type Override struct { - Id string `json:"id"` + ID string `json:"id"` Options string `json:"options"` } type Panel struct { - Title string - Targets []*Target - DataSource *DataSource - Type string GridPos *GridPos `json:"gridPos"` + DataSource *DataSource Options *Option Override *Override + Title string + Type string + Targets []*Target } var ( dataSource = &DataSource{ Type: "prometheus", - Uid: "${DS_PROMETHEUS}", + UID: "${DS_PROMETHEUS}", } durationRe = regexp.MustCompile(`duration_seconds$`) @@ -69,7 +71,7 @@ func generatePanels() []Panel { var totalsStats = []*Target{} for _, totalMetricName := range totalMetricNames { t := &Target{ - Expr: fmt.Sprintf("%s_%s", METRIC_PREFIX, totalMetricName), + Expr: fmt.Sprintf("%s_%s", MetricPrefix, totalMetricName), LegendFormat: "{{__name__}}", DataSource: dataSource, } @@ -83,7 +85,7 @@ func generatePanels() []Panel { } t := &Target{ - Expr: fmt.Sprintf("%s_%s", METRIC_PREFIX, totalStatName), + Expr: fmt.Sprintf("%s_%s", MetricPrefix, totalStatName), LegendFormat: legendFmt, DataSource: dataSource, } @@ -91,9 +93,9 @@ func generatePanels() []Panel { } var panels = []Panel{ - Panel{ + { Type: "row", - Title: fmt.Sprintf("sk8l: %s overview", K8_NAMESPACE), + Title: fmt.Sprintf("sk8l: %s overview", K8Namespace), DataSource: dataSource, GridPos: &GridPos{ H: 1, @@ -103,7 +105,7 @@ func generatePanels() []Panel { }, Targets: make([]*Target, 0), }, - Panel{ + { Title: "completed / registered / failed cronjobs totals", DataSource: dataSource, GridPos: &GridPos{ @@ -117,9 +119,9 @@ func generatePanels() []Panel { Calcs: "last", }, }, - Panel{ + { Type: "stat", - Title: fmt.Sprintf("sk8l: %s totals", K8_NAMESPACE), + Title: fmt.Sprintf("sk8l: %s totals", K8Namespace), DataSource: dataSource, GridPos: &GridPos{ H: 8, @@ -132,7 +134,7 @@ func generatePanels() []Panel { Calcs: "lastNotNull", }, Override: &Override{ - Id: "byName", + ID: "byName", Options: "failing cronjobs", }, }, @@ -142,21 +144,29 @@ func generatePanels() []Panel { individualPanelsGenerator := func(key, value any) bool { var row Panel - var target = &Target{} + var target *Target var failureMetricName string var cronjobDurations = make([]*Target, 0) var cronjobTotals = make([]*Target, 0) - metricNames := value.([]string) + metricNames, ok := value.([]string) - keyName := key.(string) + if !ok { + log.Println("Error: value.([]string)") + } + + keyName, ok := key.(string) + + if !ok { + log.Println("Error: key.(string)") + } i := len(individualPanels) var rowI, rowM, failureY uint16 rowI = uint16((i + 1) * 9) - rowM = uint16(rowI + 1) - failureY = uint16(rowM + 8) + rowM = rowI + 1 + failureY = rowM + 8 row = Panel{ Type: "row", diff --git a/k8s_api.go b/k8s_api.go index 72b15ef..3ca1162 100644 --- a/k8s_api.go +++ b/k8s_api.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "log" "slices" @@ -18,8 +17,8 @@ import ( ) type K8sClient struct { - namespace string ClientSet *kubernetes.Clientset + namespace string } // A ClientOption is used to configure a Client. @@ -63,13 +62,13 @@ func (kc *K8sClient) GetCronjob(cronjobNamespace, cronjobName string) *batchv1.C cronJob, err := kc.ClientSet.BatchV1().CronJobs(cronjobNamespace).Get(ctx, cronjobName, metav1.GetOptions{}) if errors.IsNotFound(err) { - log.Printf("Cronjob %s found in default namespace\n", cronjobName) + log.Printf("Cronjob not %s found in default namespace\n", cronjobName) } else if statusError, isStatus := err.(*errors.StatusError); isStatus { log.Printf("Error getting cronjob %v\n", statusError.ErrStatus.Message) } else if err != nil { panic(err.Error()) } else { - log.Printf("Found example-xxxxx cronjob in default namespace\n") + log.Printf("Found %s cronjob in %s namespace\n", cronjobName, cronjobNamespace) } return cronJob @@ -91,26 +90,6 @@ func (kc *K8sClient) WatchPods() watch.Interface { return watcher } -func (kc *K8sClient) GetJobPodsForJob(job *batchv1.Job) *corev1.PodList { - ctx := context.TODO() - - jobPods, err := kc.ClientSet.CoreV1().Pods(job.Namespace).List(ctx, metav1.ListOptions{ - LabelSelector: fmt.Sprintf("job-name=%s", job.Name), - }) - - if errors.IsNotFound(err) { - log.Printf("Pods found in default namespace\n") - } else if statusError, isStatus := err.(*errors.StatusError); isStatus { - log.Printf("Error getting pod %v\n", statusError.ErrStatus.Message) - } else if err != nil { - panic(err.Error()) - } else { - log.Printf("There are %d jobPods in the cluster for job %s - %s\n", len(jobPods.Items), job.UID, job.Name) - } - - return jobPods -} - func (kc *K8sClient) GetPod(jobNamespace, podName string) *corev1.Pod { ctx := context.TODO() pod, err := kc.ClientSet.CoreV1().Pods(jobNamespace).Get(ctx, podName, metav1.GetOptions{}) @@ -125,7 +104,7 @@ func (kc *K8sClient) GetPod(jobNamespace, podName string) *corev1.Pod { } else if err != nil { panic(err.Error()) } else { - log.Printf("Found example-xxxxx pod in default namespace\n") + log.Printf("Found %s pod in %s namespace\n", podName, jobNamespace) } return pod diff --git a/main.go b/main.go index 9af8a8b..431389d 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "crypto/x509" "fmt" "log" "net" @@ -21,27 +22,39 @@ import ( healthgrpc "google.golang.org/grpc/health/grpc_health_v1" ) +const ( + ReadTimeoutSeconds = 10 + WriteTimeoutSeconds = 30 + ReadTimeout = time.Duration(ReadTimeoutSeconds) + WriteTimeout = time.Duration(WriteTimeoutSeconds) +) + var ( - K8_NAMESPACE = os.Getenv("K8_NAMESPACE") - API_PORT = os.Getenv("SK8L_SERVICE_PORT_SK8L_API") - API_HEALTH_PORT = os.Getenv("SK8L_SERVICE_PORT_SK8L_API_HEALTH") - METRICS_PORT = os.Getenv("SK8L_SERVICE_PORT_SK8L_API_METRICS") - certFile = filepath.Join("/etc", "sk8l-certs", "server-cert.pem") - certKeyFile = filepath.Join("/etc", "sk8l-certs", "server-key.pem") - caFile = filepath.Join("/etc", "sk8l-certs", "ca-cert.pem") - METRIC_PREFIX = fmt.Sprintf("sk8l_%s", K8_NAMESPACE) + K8Namespace = os.Getenv("K8_NAMESPACE") + APIPort = os.Getenv("SK8L_SERVICE_PORT_SK8L_API") + APIHealthPort = os.Getenv("SK8L_SERVICE_PORT_SK8L_API_HEALTH") + MetricsPort = os.Getenv("SK8L_SERVICE_PORT_SK8L_API_METRICS") + certFile = filepath.Join("/etc", "sk8l-certs", "server-cert.pem") + certKeyFile = filepath.Join("/etc", "sk8l-certs", "server-key.pem") + caFile = filepath.Join("/etc", "sk8l-certs", "ca-cert.pem") + MetricPrefix = fmt.Sprintf("sk8l_%s", K8Namespace) ) func main() { - serverTLSConfig, err := setupTLS(certFile, certKeyFile, caFile) - target := fmt.Sprintf("0.0.0.0:%s", API_PORT) + certPool := x509.NewCertPool() + serverTLSConfig, err := setupTLS(certFile, certKeyFile, caFile, certPool) + if err != nil { + log.Fatal("Error: setupTLS:", err) + } + + target := fmt.Sprintf("0.0.0.0:%s", APIPort) conn, err := net.Listen("tcp", target) if err != nil { log.Fatal("tlsListen error:", err) } - healthConn, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", API_HEALTH_PORT)) + healthConn, err := net.Listen("tcp", fmt.Sprintf("0.0.0.0:%s", APIHealthPort)) if err != nil { log.Fatal("Health Probe Listen error:", err) @@ -54,7 +67,7 @@ func main() { probeS := grpc.NewServer() log.Printf("grpcS creds %v", creds) - k8sClient := NewK8sClient(WithNamespace(K8_NAMESPACE)) + k8sClient := NewK8sClient(WithNamespace(K8Namespace)) db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) @@ -73,10 +86,10 @@ func main() { http.Handle("/metrics", promhttp.Handler()) httpS := &http.Server{ - Addr: fmt.Sprintf("0.0.0.0:%s", METRICS_PORT), + Addr: fmt.Sprintf("0.0.0.0:%s", MetricsPort), IdleTimeout: time.Minute, - ReadTimeout: 10 * time.Second, - WriteTimeout: 30 * time.Second, + ReadTimeout: ReadTimeout * time.Second, + WriteTimeout: WriteTimeout * time.Second, TLSConfig: serverTLSConfig, } logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) @@ -97,7 +110,6 @@ func main() { if err != nil { log.Fatal("httpS error", err) } - }() go func() { diff --git a/metrics.go b/metrics.go index 0b7ce31..8a0353f 100644 --- a/metrics.go +++ b/metrics.go @@ -95,7 +95,7 @@ func recordMetrics(ctx context.Context, svr *Sk8lServer) { } if err != nil { - log.Fatalf("%v.GetCronjobs(_) = _, %v", cronjobsClient, err) + log.Printf("Error: %v.GetCronjobs(_) = _, %v\n", cronjobsClient, err) } registeredCronjobs := len(cronjobsResponse.Cronjobs) @@ -111,9 +111,9 @@ func recordMetrics(ctx context.Context, svr *Sk8lServer) { durationMetricName := fmt.Sprintf("%s_duration_seconds", sanitizedCjName) metricNames = []string{ - fmt.Sprintf("%s_%s", METRIC_PREFIX, completionMetricName), - fmt.Sprintf("%s_%s", METRIC_PREFIX, failureMetricName), - fmt.Sprintf("%s_%s", METRIC_PREFIX, durationMetricName), + fmt.Sprintf("%s_%s", MetricPrefix, completionMetricName), + fmt.Sprintf("%s_%s", MetricPrefix, failureMetricName), + fmt.Sprintf("%s_%s", MetricPrefix, durationMetricName), } metricsNamesMap.Store(sanitizedCjName, metricNames) diff --git a/sk8l.go b/sk8l.go index 5da2711..74171db 100644 --- a/sk8l.go +++ b/sk8l.go @@ -21,7 +21,7 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/protoadapt" - // structpb "google.golang.org/protobuf/types/known/structpb" + // structpb "google.golang.org/protobuf/types/known/structpb". "google.golang.org/grpc" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -30,13 +30,17 @@ import ( ) const ( - jobPodsKeyFmt = "jobs_pods_for_job_%s" - cronjobsKeyFmt = "sk8l_cronjob_%s_%s" + jobPodsKeyFmt = "jobs_pods_for_job_%s" + cronjobsKeyFmt = "sk8l_cronjob_%s_%s" + badgerTTLSeconds = 15 + refreshSeconds = 10 ) var ( cronjobsCacheKey = []byte("sk8l_cronjobs") jobsCacheKey = []byte("sk8l_jobs") + badgerTTL = time.Duration(badgerTTLSeconds) + refreshInterval = time.Second * refreshSeconds ) type Sk8lServer struct { @@ -50,7 +54,10 @@ type Sk8lServer struct { type APICall (func() []byte) -func (s Sk8lServer) Check(ctx context.Context, req *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { +func (s Sk8lServer) Check( + ctx context.Context, + req *grpc_health_v1.HealthCheckRequest, +) (*grpc_health_v1.HealthCheckResponse, error) { log.Default().Println("serving health") return &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil } @@ -59,6 +66,7 @@ func (s Sk8lServer) Watch(req *grpc_health_v1.HealthCheckRequest, stream grpc_he response := &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING} if err := stream.Send(response); err != nil { + log.Println("Error: Watch#stream.Send", err) return err } @@ -104,7 +112,7 @@ func (s *Sk8lServer) GetCronjobs(in *protos.CronjobsRequest, stream protos.Cronj return err } - time.Sleep(time.Second * 10) + time.Sleep(refreshInterval) } } @@ -119,7 +127,7 @@ func (s *Sk8lServer) GetCronjob(in *protos.CronjobRequest, stream protos.Cronjob return err } - time.Sleep(time.Second * 10) + time.Sleep(refreshInterval) } } @@ -153,15 +161,18 @@ func (s *Sk8lServer) GetCronjobPods(in *protos.CronjobPodsRequest, stream protos return err } - time.Sleep(time.Second * 10) + time.Sleep(refreshInterval) } } -func (s *Sk8lServer) GetCronjobYAML(ctx context.Context, in *protos.CronjobRequest) (*protos.CronjobYAMLResponse, error) { +func (s *Sk8lServer) GetCronjobYAML( + ctx context.Context, + in *protos.CronjobRequest, +) (*protos.CronjobYAMLResponse, error) { cronjob := s.K8sClient.GetCronjob(in.CronjobNamespace, in.CronjobName) - prettyJson, _ := json.MarshalIndent(cronjob, "", " ") + prettyJSON, _ := json.MarshalIndent(cronjob, "", " ") - y, _ := gyaml.JSONToYAML(prettyJson) + y, _ := gyaml.JSONToYAML(prettyJSON) response := &protos.CronjobYAMLResponse{ Cronjob: string(y), @@ -172,9 +183,9 @@ func (s *Sk8lServer) GetCronjobYAML(ctx context.Context, in *protos.CronjobReque func (s *Sk8lServer) GetJobYAML(ctx context.Context, in *protos.JobRequest) (*protos.JobYAMLResponse, error) { job := s.K8sClient.GetJob(in.JobNamespace, in.JobName) - prettyJson, _ := json.MarshalIndent(job, "", " ") + prettyJSON, _ := json.MarshalIndent(job, "", " ") - y, _ := gyaml.JSONToYAML(prettyJson) + y, _ := gyaml.JSONToYAML(prettyJSON) response := &protos.JobYAMLResponse{ Job: string(y), @@ -185,9 +196,9 @@ func (s *Sk8lServer) GetJobYAML(ctx context.Context, in *protos.JobRequest) (*pr func (s *Sk8lServer) GetPodYAML(ctx context.Context, in *protos.PodRequest) (*protos.PodYAMLResponse, error) { pod := s.K8sClient.GetPod(in.PodNamespace, in.PodName) - prettyJson, _ := json.MarshalIndent(pod, "", " ") + prettyJSON, _ := json.MarshalIndent(pod, "", " ") - y, _ := gyaml.JSONToYAML(prettyJson) + y, _ := gyaml.JSONToYAML(prettyJSON) response := &protos.PodYAMLResponse{ Pod: string(y), @@ -196,13 +207,16 @@ func (s *Sk8lServer) GetPodYAML(ctx context.Context, in *protos.PodRequest) (*pr return response, nil } -func (s *Sk8lServer) GetDashboardAnnotations(context.Context, *protos.DashboardAnnotationsRequest) (*protos.DashboardAnnotationsResponse, error) { +func (s *Sk8lServer) GetDashboardAnnotations( + context.Context, + *protos.DashboardAnnotationsRequest, +) (*protos.DashboardAnnotationsResponse, error) { panels := generatePanels() var tmplFile = "annotations.tmpl" t := template.New(tmplFile) // t = t.Funcs(template.FuncMap{"StringsJoin": strings.Join}) - t = t.Funcs(template.FuncMap{"marshal": func(v interface{}) string { + t = t.Funcs(template.FuncMap{"marshal": func(v any) string { a, _ := json.Marshal(v) return string(a) }, @@ -231,12 +245,14 @@ func (s *Sk8lServer) getAndStore(key []byte, apiCall APICall) ([]byte, error) { if errors.Is(err, badger.ErrKeyNotFound) { err = s.DB.Update(func(txn *badger.Txn) error { apiResult := apiCall() - entry := badger.NewEntry(key, apiResult).WithTTL(time.Second * 15) - err := txn.SetEntry(entry) + entry := badger.NewEntry(key, apiResult).WithTTL(time.Second * badgerTTL) + err = txn.SetEntry(entry) + if err != nil { + log.Println("Error: getAndStore#txn.SetEntry", err) + } valueResponse = append([]byte{}, apiResult...) return err }) - } else { err = item.Value(func(val []byte) error { valueResponse = append([]byte{}, val...) @@ -280,8 +296,8 @@ func (s *Sk8lServer) findCronjobs() *batchv1.CronJobList { value, err := s.get(cronjobsCacheKey) if err != nil { - if errors.Is(err, badger.ErrKeyNotFound) { - + if !errors.Is(err, badger.ErrKeyNotFound) { + log.Println("Error: findCronjobs#s.get", err) } } @@ -290,7 +306,7 @@ func (s *Sk8lServer) findCronjobs() *batchv1.CronJobList { err = proto.Unmarshal(value, cronjobListV2) if err != nil { - log.Fatalln("findCronjobs", err) + log.Println("Error: findCronjobs#proto.Unmarshal", err) } return cronjobList @@ -310,7 +326,7 @@ func (s *Sk8lServer) findCronjob(cronjobNamespace, cronjobName string) *batchv1. cronjobValue, err := s.getAndStore(key, gCjCall) if err != nil { - + log.Println("Error: findCronjob#s.getAndStore", err) } cronjob := &batchv1.CronJob{} @@ -318,7 +334,7 @@ func (s *Sk8lServer) findCronjob(cronjobNamespace, cronjobName string) *batchv1. err = proto.Unmarshal(cronjobValue, cronjobV2) if err != nil { - + log.Println("Error: findCronjob#proton.Unmarshal", err) } return cronjob @@ -334,7 +350,7 @@ func (s *Sk8lServer) findJobs() *protos.MappedJobs { value, err := s.getAndStore(jobsCacheKey, jobsCall) if err != nil { - + log.Println("Error: findJobs#s.getAndStore", err) } mappedJobs := &protos.MappedJobs{} @@ -342,7 +358,7 @@ func (s *Sk8lServer) findJobs() *protos.MappedJobs { err = proto.Unmarshal(value, mappedJobs) if err != nil { - + log.Println("Error: findJobs#proton.Unmarshal", err) } return mappedJobs @@ -354,22 +370,36 @@ func (s *Sk8lServer) findJobPodsForJob(job *batchv1.Job) *corev1.PodList { collection := &corev1.PodList{} collectionV2 := protoadapt.MessageV2Of(collection) - s.DB.View(func(txn *badger.Txn) error { + err := s.DB.View(func(txn *badger.Txn) error { current, err := txn.Get(key) if errors.Is(err, badger.ErrKeyNotFound) { return nil } - current.Value(func(val []byte) error { - proto.Unmarshal(val, collectionV2) + err = current.Value(func(val []byte) error { + err = proto.Unmarshal(val, collectionV2) + + if err != nil { + log.Println("findJobPodsForJob#proto.Unmarshal", err) + return err + } return nil }) + if err != nil { + log.Println("Error: findJobPodsForJob#current.Value", err) + return err + } + return nil }) + if err != nil { + log.Println("Error: findJobPodsForJob#DB.View", err) + } + podItems := []corev1.Pod{} podMap := make(map[string][]corev1.Pod) @@ -398,15 +428,20 @@ func (s *Sk8lServer) findJobPodsForJob(job *batchv1.Job) *corev1.PodList { return podList } -// Revisit this. JobConditions are not being used yet anywhere. PodResponse.TerminationReasons.TerminationDetails -> ContainerStateTerminated -func jobFailed(job *batchv1.Job, jobPodsResponses []*protos.PodResponse) (bool, *batchv1.JobCondition, []*batchv1.JobCondition) { +// Revisit this. JobConditions are not being used yet anywhere. +// PodResponse.TerminationReasons.TerminationDetails -> ContainerStateTerminated. +func jobFailed( + job *batchv1.Job, + jobPodsResponses []*protos.PodResponse, +) (bool, *batchv1.JobCondition, []*batchv1.JobCondition) { var jobFailed bool var failureCondition *batchv1.JobCondition n := len(job.Status.Conditions) jobConditions := make([]*batchv1.JobCondition, 0, n) - for _, jobCondition := range job.Status.Conditions { - if jobFailed != true { + for i := range job.Status.Conditions { + jobCondition := job.Status.Conditions[i] + if !jobFailed { if jobCondition.Type == batchv1.JobFailed { jobFailed = true failureCondition = &jobCondition @@ -477,7 +512,11 @@ func (s *Sk8lServer) collectCronjobs() { for { event, more := <-x.ResultChan() if more { - eventCronjob := event.Object.(*batchv1.CronJob) + eventCronjob, ok := event.Object.(*batchv1.CronJob) + + if !ok { + log.Println("Error: event.Object.(*batchv1.CronJob)") + } err := s.DB.Update(func(txn *badger.Txn) error { item, err := txn.Get(cronjobsCacheKey) @@ -488,8 +527,12 @@ func (s *Sk8lServer) collectCronjobs() { cjListV2 := protoadapt.MessageV2Of(cjList) result, _ := proto.Marshal(cjListV2) + entry := badger.NewEntry(cronjobsCacheKey, result) - err := txn.SetEntry(entry) + err = txn.SetEntry(entry) + if err != nil { + log.Println("Error: collectCronjobs#txn.SetEntry", err) + } return err } @@ -497,7 +540,11 @@ func (s *Sk8lServer) collectCronjobs() { storedCjList := &batchv1.CronJobList{} storedCjListV2 := protoadapt.MessageV2Of(storedCjList) - proto.Unmarshal(stored, storedCjListV2) + err = proto.Unmarshal(stored, storedCjListV2) + + if err != nil { + log.Println("Error: collectCronjobs#proto.Unmarshal", err) + } switch event.Type { case "ADDED": @@ -510,10 +557,16 @@ func (s *Sk8lServer) collectCronjobs() { updateStoredCronjobList(storedCjList, eventCronjob) } - result, _ := proto.Marshal(storedCjListV2) + result, err := proto.Marshal(storedCjListV2) + if err != nil { + log.Println("Error: collectCronjobs#proto.Marshal", err) + } entry := badger.NewEntry(cronjobsCacheKey, result) - err := txn.SetEntry(entry) + err = txn.SetEntry(entry) + if err != nil { + log.Println("Error: collectCronjobs#txn.SetEntry", err) + } return err }) @@ -523,7 +576,6 @@ func (s *Sk8lServer) collectCronjobs() { if err != nil { panic(err) } - } else { x = s.K8sClient.WatchCronjobs() log.Println("Cronjob watching: Received all Cronjobs. Opening again") @@ -539,7 +591,11 @@ func (s *Sk8lServer) collectPods() { for { event, more := <-x.ResultChan() if more { - eventPod := event.Object.(*corev1.Pod) + eventPod, ok := event.Object.(*corev1.Pod) + + if !ok { + log.Println("Error: event.Object.(*corev1.Pod)") + } fKey := fmt.Sprintf(jobPodsKeyFmt, eventPod.Labels["job-name"]) key := []byte(fKey) @@ -551,22 +607,38 @@ func (s *Sk8lServer) collectPods() { } podListV2 := protoadapt.MessageV2Of(podList) - result, _ := proto.Marshal(podListV2) + result, err := proto.Marshal(podListV2) + + if err != nil { + log.Println("Error: collectPods#proto.Marshal", err) + } + entry := badger.NewEntry(key, result) - err := txn.SetEntry(entry) + err = txn.SetEntry(entry) return err } err = item.Value(func(val []byte) error { podList := &corev1.PodList{} podListV2 := protoadapt.MessageV2Of(podList) - proto.Unmarshal(val, podListV2) + err = proto.Unmarshal(val, podListV2) + + if err != nil { + log.Println("Error: collectPods#proto.Unmarshal", err) + } podList.Items = append(podList.Items, *eventPod) - result, _ := proto.Marshal(podListV2) + result, err := proto.Marshal(podListV2) + + if err != nil { + log.Println("Error: collectPods#proto.Marshal", err) + } entry := badger.NewEntry(key, result) - err := txn.SetEntry(entry) + err = txn.SetEntry(entry) + if err != nil { + log.Println("Error: collectCronjobs#txn.SetEntry", err) + } return err }) @@ -584,7 +656,10 @@ func (s *Sk8lServer) collectPods() { }() } -func (s *Sk8lServer) allAndRunningJobsAnPods(jobs []*batchv1.Job, cronjobUID types.UID) ([]*protos.JobResponse, []*protos.PodResponse, []*protos.JobResponse, []*protos.PodResponse) { +func (s *Sk8lServer) allAndRunningJobsAnPods( + jobs []*batchv1.Job, + cronjobUID types.UID, +) ([]*protos.JobResponse, []*protos.PodResponse, []*protos.JobResponse, []*protos.PodResponse) { jn := len(jobs) allJobsForCronJob := make([]*protos.JobResponse, 0, jn) allJobPodsForCronjob := make([]*protos.PodResponse, 0) @@ -641,7 +716,10 @@ func (s *Sk8lServer) jobsForCronjob(jobsMapped *protos.MappedJobs, cronjobName s } func (s *Sk8lServer) cronJobResponse(cronJob batchv1.CronJob, jobsForCronjob []*batchv1.Job) *protos.CronjobResponse { - allJobsForCronJob, jobPodsForCronJob, runningJobs, runningJobPods := s.allAndRunningJobsAnPods(jobsForCronjob, cronJob.UID) + allJobsForCronJob, jobPodsForCronJob, runningJobs, runningJobPods := s.allAndRunningJobsAnPods( + jobsForCronjob, + cronJob.UID, + ) lastDuration := getLastDuration(allJobsForCronJob) currentDuration := getCurrentDuration(runningJobs) @@ -679,7 +757,11 @@ func (s *Sk8lServer) cronJobResponse(cronJob batchv1.CronJob, jobsForCronjob []* return cjr } -func collectTerminatedAndFailedContainers(pod *corev1.Pod, statuses []corev1.ContainerStatus, terminationReasons *[]*protos.TerminationReason) ([]*protos.ContainerResponse, []*protos.ContainerResponse) { +func collectTerminatedAndFailedContainers( + pod *corev1.Pod, + statuses []corev1.ContainerStatus, + terminationReasons *[]*protos.TerminationReason, +) ([]*protos.ContainerResponse, []*protos.ContainerResponse) { terminatedContainers := make([]*protos.ContainerResponse, 0) failedContainers := make([]*protos.ContainerResponse, 0) @@ -741,12 +823,26 @@ func collectTerminatedAndFailedContainers(pod *corev1.Pod, statuses []corev1.Con return terminatedContainers, failedContainers } -func terminatedAndFailedContainersResponses(pod *corev1.Pod) (*protos.TerminatedContainers, *protos.TerminatedContainers) { +func terminatedAndFailedContainersResponses( + pod *corev1.Pod, +) (*protos.TerminatedContainers, *protos.TerminatedContainers) { terminatedReasons := make([]*protos.TerminationReason, 0) - terminatedEphContainers, failedEphContainers := collectTerminatedAndFailedContainers(pod, pod.Status.EphemeralContainerStatuses, &terminatedReasons) - terminatedInitContainers, failedInitContainers := collectTerminatedAndFailedContainers(pod, pod.Status.InitContainerStatuses, &terminatedReasons) - terminatedContainers, failedContainers := collectTerminatedAndFailedContainers(pod, pod.Status.ContainerStatuses, &terminatedReasons) + terminatedEphContainers, failedEphContainers := collectTerminatedAndFailedContainers( + pod, + pod.Status.EphemeralContainerStatuses, + &terminatedReasons, + ) + terminatedInitContainers, failedInitContainers := collectTerminatedAndFailedContainers( + pod, + pod.Status.InitContainerStatuses, + &terminatedReasons, + ) + terminatedContainers, failedContainers := collectTerminatedAndFailedContainers( + pod, + pod.Status.ContainerStatuses, + &terminatedReasons, + ) terminatedContainersResponse := &protos.TerminatedContainers{ InitContainers: terminatedInitContainers, @@ -817,9 +913,9 @@ func jobSucceded(job *batchv1.Job) bool { return job.Status.CompletionTime != nil } -func buildLastTimes(cronJob batchv1.CronJob) (string, string) { - var lastSuccessfulTime string - var lastScheduleTime string +func buildLastTimes(cronJob batchv1.CronJob) (lastSuccessfulTime string, lastScheduleTime string) { + // var lastSuccessfulTime string + // var lastScheduleTime string if cronJob.Status.LastSuccessfulTime != nil { lastSuccessfulTime = cronJob.Status.LastSuccessfulTime.UTC().Format(time.RFC3339) } @@ -846,7 +942,10 @@ func buildCronJobCommand(cronJob batchv1.CronJob) map[string]*protos.ContainerCo var command bytes.Buffer for _, container := range cronJob.Spec.JobTemplate.Spec.Template.Spec.InitContainers { for _, ccmd := range container.Command { - command.WriteString(fmt.Sprintf("%s ", ccmd)) + _, err := command.WriteString(fmt.Sprintf("%s ", ccmd)) + if err != nil { + log.Println("Error: buildCronJobCommand#command.WriteString") + } } initContainersCommands = append(initContainersCommands, command.String()) command.Reset() @@ -855,7 +954,10 @@ func buildCronJobCommand(cronJob batchv1.CronJob) map[string]*protos.ContainerCo containersCommands := make([]string, 0, n) for _, container := range cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers { for _, ccmd := range container.Command { - command.WriteString(fmt.Sprintf("%s ", ccmd)) + _, err := command.WriteString(fmt.Sprintf("%s ", ccmd)) + if err != nil { + log.Println("Error: buildCronJobCommand#command.WriteString") + } } containersCommands = append(containersCommands, command.String()) command.Reset() @@ -864,7 +966,10 @@ func buildCronJobCommand(cronJob batchv1.CronJob) map[string]*protos.ContainerCo ephemeralContainersinersCommands := make([]string, 0, n) for _, container := range cronJob.Spec.JobTemplate.Spec.Template.Spec.EphemeralContainers { for _, ccmd := range container.Command { - command.WriteString(fmt.Sprintf("%s ", ccmd)) + _, err := command.WriteString(fmt.Sprintf("%s ", ccmd)) + if err != nil { + log.Println("Error: buildCronJobCommand#command.WriteString") + } } ephemeralContainersinersCommands = append(ephemeralContainersinersCommands, command.String()) command.Reset() @@ -883,7 +988,7 @@ func buildCronJobCommand(cronJob batchv1.CronJob) map[string]*protos.ContainerCo return commands } -func updateStoredCronjobList(storedCjList *batchv1.CronJobList, eventCronjob *batchv1.CronJob) *batchv1.CronJobList { +func updateStoredCronjobList(storedCjList *batchv1.CronJobList, eventCronjob *batchv1.CronJob) { cjList := &batchv1.CronJobList{} // to avoid duplicates if the process is restarted and on "MODIFIED" to get the updated version of the resource for _, cronjob := range storedCjList.Items { @@ -892,8 +997,6 @@ func updateStoredCronjobList(storedCjList *batchv1.CronJobList, eventCronjob *ba } } storedCjList.Items = cjList.Items - - return storedCjList } func getCurrentDuration(runningJobsForCronJob []*protos.JobResponse) int64 { diff --git a/tls.go b/tls.go index 797564c..0e02b77 100644 --- a/tls.go +++ b/tls.go @@ -3,12 +3,41 @@ package main import ( "crypto/tls" "crypto/x509" + "errors" "fmt" - "io/ioutil" + "os" + "path/filepath" ) -func setupTLS(certFile, certKeyFile, caFile string) (*tls.Config, error) { - tlsConfig := &tls.Config{} +type CertPool interface { + AppendCertsFromPEM(caBytes []byte) bool +} + +var ( + ErrFailedToAppend = errors.New("failed to append Root certificate") + ErrTLS = errors.New("TLS Error") + ErrCertPool = errors.New("certPool Error") + ErrCACertFile = errors.New("caCertFile Error") +) + +type CertError struct { + Err error + CertFile string + Msg string +} + +func (ce *CertError) Error() string { + return fmt.Sprintf("CertError: %s - %s (%s)", ce.Err.Error(), ce.Msg, ce.CertFile) +} + +func (ce *CertError) Unwrap() error { + return fmt.Errorf(" %w: %s", ce.Err, ce.Msg) +} + +func setupTLS(certFile, certKeyFile, caFile string, certPool CertPool) (*tls.Config, error) { + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS13, + } var err error tlsConfig.Certificates = make([]tls.Certificate, 1) @@ -18,22 +47,47 @@ func setupTLS(certFile, certKeyFile, caFile string) (*tls.Config, error) { ) if err != nil { - return nil, err + cErr := &CertError{ + CertFile: certFile, + Msg: err.Error(), + Err: ErrTLS, + } + return nil, cErr } - caBytes, err := ioutil.ReadFile(caFile) + caBytes, err := os.ReadFile(filepath.Clean(caFile)) + + if err != nil { + cErr := &CertError{ + CertFile: certFile, + Msg: err.Error(), + Err: ErrCACertFile, + } + return nil, cErr + } + + ok := certPool.AppendCertsFromPEM(caBytes) + + if !ok { + cErr := &CertError{ + CertFile: certFile, + Err: ErrFailedToAppend, + } + return nil, cErr + } - certPool := x509.NewCertPool() - ok := certPool.AppendCertsFromPEM([]byte(caBytes)) + certPoolAsX509CertPool, ok := certPool.(*x509.CertPool) if !ok { - return nil, fmt.Errorf( - "failed to parse root certificate: %q", certFile, - ) + cErr := &CertError{ + CertFile: certFile, + Err: ErrCertPool, + } + return nil, cErr } - tlsConfig.ClientCAs = certPool - tlsConfig.RootCAs = certPool + tlsConfig.ClientCAs = certPoolAsX509CertPool + tlsConfig.RootCAs = certPoolAsX509CertPool tlsConfig.ServerName = "0.0.0.0" return tlsConfig, nil diff --git a/tls_test.go b/tls_test.go new file mode 100644 index 0000000..24316a1 --- /dev/null +++ b/tls_test.go @@ -0,0 +1,128 @@ +package main + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "testing" +) + +type CustomCertPool struct { + certPool *x509.CertPool +} + +func (c *CustomCertPool) AppendCertsFromPEM(caBytes []byte) bool { + return false +} + +func TestSetupTLS(t *testing.T) { + testCases := []struct { + certPool CertPool + expectedError error + expectedErrMsg string + name string + certFile string + keyFile string + caFile string + expectedSN string + expectedMinVer uint16 + expectedCAs bool + }{ + { + name: "ValidFiles", + certFile: "testdata/server-cert.pem", + keyFile: "testdata/server-key.key", + caFile: "testdata/ca-cert.pem", + certPool: x509.NewCertPool(), + expectedError: nil, + expectedErrMsg: "", + expectedMinVer: tls.VersionTLS13, + expectedCAs: true, + expectedSN: "0.0.0.0", + }, + { + name: "InvalidCA", + certFile: "testdata/invalid-cert.pem", + keyFile: "testdata/server-key.key", + caFile: "testdata/ca-cert.pem", + certPool: &x509.CertPool{}, + expectedError: ErrTLS, + expectedErrMsg: "CertError: TLS Error - tls: private key does not match public key (testdata/invalid-cert.pem)", + expectedMinVer: 0, + expectedCAs: false, + expectedSN: "", + }, + { + name: "FailedToAppend", + certFile: "testdata/server-cert.pem", + keyFile: "testdata/server-key.key", + caFile: "testdata/ca-cert.pem", + certPool: &CustomCertPool{certPool: x509.NewCertPool()}, + expectedError: ErrFailedToAppend, + expectedErrMsg: "CertError: failed to append Root certificate - (testdata/server-cert.pem)", + expectedMinVer: 0, + expectedCAs: false, + expectedSN: "", + }, + { + name: "MissingCACert", + certFile: "testdata/server-cert.pem", + keyFile: "testdata/server-key.key", + caFile: "testdata/no-ca-cert.pem", + certPool: x509.NewCertPool(), + expectedError: ErrCACertFile, + expectedErrMsg: "CertError: caCertFile Error - open testdata/no-ca-cert.pem: no such file or directory (testdata/server-cert.pem)", + expectedMinVer: 0, + expectedCAs: false, + expectedSN: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tlsConfig, err := setupTLS(tc.certFile, tc.keyFile, tc.caFile, tc.certPool) + + if err != nil { + if !errors.Is(err, tc.expectedError) { + t.Errorf("expected error to be %v, got %v", tc.expectedError, err) + } + + if tc.expectedError != nil { + certError, ok := err.(*CertError) + if !ok { + t.Errorf("unexpected error type: got %T, want *CertError", err) + return + } + + if !errors.Is(certError.Err, tc.expectedError) { + t.Errorf("unexpected error type: got %v, want %v", certError.Err, tc.expectedError) + } + + if err.Error() != tc.expectedErrMsg { + t.Errorf("unexpected error message: got %s, want %s", err.Error(), tc.expectedErrMsg) + } + } + } else { + if tlsConfig.MinVersion != tc.expectedMinVer { + t.Error("Unexpected MinVersion value") + } + + if tlsConfig.ClientCAs == nil || tlsConfig.RootCAs == nil { + if tc.expectedCAs { + t.Error("Expected ClientCAs and RootCAs to be non-nil") + } + } else { + if tlsConfig.ClientCAs != tc.certPool || tlsConfig.RootCAs != tc.certPool { + if tc.expectedCAs { + t.Error("ClientCAs and RootCAs should match the passed certPool") + } + } + } + + if tlsConfig.ServerName != tc.expectedSN { + t.Error("Unexpected ServerName value") + } + } + }) + } +} diff --git a/version.go b/version.go index 93fc24d..6d29038 100644 --- a/version.go +++ b/version.go @@ -1,6 +1,6 @@ package main -// Set with LDFLAGS +// Set with LDFLAGS. var version = "unset" func Version() string {