diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 5abaa162..90a3f491 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -27,10 +27,14 @@ jobs: - name: Run static checks uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 with: - version: v2.1.6 + version: v2.5.0 # use our .golangci.yml args: --config=.golangci.yml --verbose skip-cache: true + - name: Run the slowg linter + run: | + go install github.com/cilium/linters@latest + linters -slowg ./... - name: govulncheck uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee with: diff --git a/.golangci.yml b/.golangci.yml index 3153e02c..e416f0e6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -9,45 +9,62 @@ linters: - asciicheck - bidichk - bodyclose + - containedctx - contextcheck - copyloopvar - decorder + - dogsled + - dupl - dupword - durationcheck - err113 - errchkjson - errname + - errorlint + - exhaustive - exptostd + - forbidigo + - forcetypeassert - gocheckcompilerdirectives + - goconst - gocritic + - godoclint + - godot - goheader + - gomodguard - goprintffuncname - gosec - govet + - grouper - importas - ineffassign - interfacebloat - intrange + - iotamixing + - ireturn - makezero - mirror - misspell - musttag - nakedret + - nestif - nilerr + - nilnil - noctx - nosprintfhostport - perfsprint + - prealloc - predeclared - promlinter - protogetter - reassign - revive - - rowserrcheck - sloglint - - sqlclosecheck - staticcheck - tagalign - testifylint + - thelper + - tparallel - unconvert - unparam - unused @@ -59,9 +76,42 @@ linters: disabled-checks: - exitAfterDefer - singleCaseSwitch + godoclint: + default: all + disable: + - require-pkg-doc + options: + require-doc: + # Ignore unexported (private) symbols when applying the `require-doc` rule. + ignore-unexported: true goheader: template-path: ./HEADER + gomodguard: + blocked: + modules: + - github.com/goccy/go-yaml: + recommendations: + - go.yaml.in/yaml/v3 + reason: "Let's consolidate on a single YAML library that is also used by most of our dependencies" + - gopkg.in/yaml.v2: + recommendations: + - go.yaml.in/yaml/v3 + reason: "gopkg.in/yaml.v2 is unmaintained" + - gopkg.in/yaml.v3: + recommendations: + - go.yaml.in/yaml/v3 + reason: "gopkg.in/yaml.v3 is unmaintained" + - go.uber.org/multierr: + recommendations: + - errors + reason: "Go 1.20+ has support for combining multiple errors, see https://go.dev/doc/go1.20#errors" + - go.yaml.in/yaml/v2: + recommendations: + - go.yaml.in/yaml/v3 + reason: "We are using v3" gosec: + # available rules: https://github.com/securego/gosec#available-rules + includes: [] # include all available rules excludes: - G104 # Audit errors not checked - G307 # Deferring a method which returns an error @@ -88,7 +138,15 @@ linters: - name: package-comments disabled: true sloglint: + no-mixed-args: true + no-global: all static-msg: true + key-naming-case: kebab # be consistent with key names + forbidden-keys: # let's no use reserved log keys + - level + - msg + - source + - time exclusions: rules: - linters: @@ -100,3 +158,14 @@ formatters: enable: - gofmt - goimports + settings: + goimports: + local-prefixes: + - github.com/cilium/certgen/ +issues: + # 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 diff --git a/Dockerfile b/Dockerfile index b3aa5c14..854b2378 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # (first line comment needed for DOCKER_BUILDKIT use) # # use skopeo inspect to get the multiarch manifest list digest -# skopeo inspect --override-os linux docker://golang:1.24.5-alpine3.22 | jq -r '.Digest' -ARG GOLANG_IMAGE=docker.io/library/golang:1.24.5-alpine3.22@sha256:daae04ebad0c21149979cd8e9db38f565ecefd8547cf4a591240dc1972cf1399 +# skopeo inspect --override-os linux docker://golang:1.25.3-alpine3.22 | jq -r '.Digest' +ARG GOLANG_IMAGE=docker.io/library/golang:1.25.3-alpine3.22@sha256:aee43c3ccbf24fdffb7295693b6e33b21e01baec1b2a55acc351fde345e9ec34 ARG BASE_IMAGE=scratch diff --git a/cmd/certgen.go b/cmd/certgen.go index db03f965..ea085cbc 100644 --- a/cmd/certgen.go +++ b/cmd/certgen.go @@ -29,10 +29,9 @@ import ( const binaryName = "cilium-certgen" -var log = logging.DefaultLogger.With(slog.String(logfields.LogSubsys, binaryName)) - // New creates and returns a certgen command. func New() (*cobra.Command, error) { + log := logging.Logger.With(slog.String(logfields.LogSubsys, binaryName)) vp := viper.New() rootCmd := &cobra.Command{ Use: binaryName + " [flags]", @@ -51,7 +50,7 @@ func New() (*cobra.Command, error) { "version", version.Version, ) - if err := generateCertificates(); err != nil { + if err := generateCertificates(log); err != nil { log.Error("failed to generate certificates", "error", err) } }, @@ -135,8 +134,8 @@ func parseCertificateConfigs(cfg, cfgfile string) (certConfigs option.Certificat return certConfigs, nil } -// generateCertificates runs the main code to generate and store certificate -func generateCertificates() error { +// generateCertificates runs the main code to generate and store certificate. +func generateCertificates(log *slog.Logger) error { k8sClient, err := k8sConfig(option.Config.K8sKubeConfigPath) if err != nil { return fmt.Errorf("failed initialize kubernetes client: %w", err) @@ -152,15 +151,16 @@ func generateCertificates() error { ca := generate.NewCA(option.Config.CASecretName, option.Config.CASecretNamespace) - if option.Config.CAGenerate { - err = ca.Generate(option.Config.CACommonName, option.Config.CAValidityDuration) + switch { + case option.Config.CAGenerate: + err = ca.Generate(log, option.Config.CACommonName, option.Config.CAValidityDuration) if err != nil { return fmt.Errorf("failed to generate CA: %w", err) } ctx, cancel := context.WithTimeout(context.Background(), option.Config.K8sRequestTimeout) defer cancel() - err = ca.StoreAsSecret(ctx, k8sClient, !option.Config.CAReuseSecret) + err = ca.StoreAsSecret(ctx, log, k8sClient, !option.Config.CAReuseSecret) if err != nil { if !k8sErrors.IsAlreadyExists(err) || !option.Config.CAReuseSecret { return fmt.Errorf("failed to create secret for CA: %w", err) @@ -170,7 +170,7 @@ func generateCertificates() error { } else { count++ } - } else if option.Config.CACertFile != "" && option.Config.CAKeyFile != "" { + case option.Config.CACertFile != "" && option.Config.CAKeyFile != "": log.Info("Loading CA from file") err = ca.LoadFromFile(option.Config.CACertFile, option.Config.CAKeyFile) if err != nil { @@ -204,7 +204,7 @@ func generateCertificates() error { cfg.Namespace, ).WithHosts(cfg.Hosts) - err := certs[i].Generate(ca) + err := certs[i].Generate(log, ca) if err != nil { return fmt.Errorf("failed to generate cert: %w", err) } @@ -219,7 +219,7 @@ func generateCertificates() error { ctx, cancel := context.WithTimeout(context.Background(), option.Config.K8sRequestTimeout) defer cancel() - if err := cert.StoreAsSecret(ctx, k8sClient); err != nil { + if err := cert.StoreAsSecret(ctx, log, k8sClient); err != nil { return fmt.Errorf("failed to create secret: %w", err) } diff --git a/go.mod b/go.mod index 1a3d09f4..9140f6be 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/cilium/certgen -go 1.24.5 +go 1.25.3 require ( github.com/cloudflare/cfssl v1.6.5 diff --git a/internal/generate/generate.go b/internal/generate/generate.go index e702f25d..46d8d43f 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -25,14 +25,9 @@ import ( meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" - "github.com/cilium/certgen/internal/logging" "github.com/cilium/certgen/internal/logging/logfields" ) -var ( - log = logging.DefaultLogger.With(logfields.LogSubsys, "generate") -) - // Cert contains the data and metadata of the certificate and keyfile. type Cert struct { CommonName string @@ -47,7 +42,7 @@ type Cert struct { KeyBytes []byte } -// NewCert creates a new certificate blueprint +// NewCert creates a new certificate blueprint. func NewCert( commonName string, validityDuration time.Duration, @@ -65,14 +60,15 @@ func NewCert( } } -// WithHosts modifies to use the given hosts instead of the default (CommonName) +// WithHosts modifies to use the given hosts instead of the default +// (CommonName). func (c *Cert) WithHosts(hosts []string) *Cert { c.Hosts = hosts return c } -// Generate the certificate and keyfile and populate c.CertBytes and c.CertKey -func (c *Cert) Generate(ca *CA) error { +// Generate the certificate and keyfile and populate c.CertBytes and c.CertKey. +func (c *Cert) Generate(log *slog.Logger, ca *CA) error { log.Info("Creating CSR for certificate", logfields.CertCommonName, c.CommonName, logfields.CertValidityDuration, c.ValidityDuration.String(), @@ -123,8 +119,8 @@ func (c *Cert) Generate(ca *CA) error { return nil } -// StoreAsSecret creates or updates the certificate and keyfile in a K8s secret -func (c *Cert) StoreAsSecret(ctx context.Context, k8sClient *kubernetes.Clientset) error { +// StoreAsSecret creates or updates the certificate and keyfile in a K8s secret. +func (c *Cert) StoreAsSecret(ctx context.Context, log *slog.Logger, k8sClient *kubernetes.Clientset) error { if c.CertBytes == nil || c.KeyBytes == nil { return fmt.Errorf("cannot create secret %s/%s from empty certificate", c.Namespace, c.Name) @@ -158,7 +154,7 @@ func (c *Cert) StoreAsSecret(ctx context.Context, k8sClient *kubernetes.Clientse return err } -// CA contains the data and metadata of the certificate authority +// CA contains the data and metadata of the certificate authority. type CA struct { SecretName string SecretNamespace string @@ -172,7 +168,7 @@ type CA struct { loadedFromSecret bool } -// NewCA creates a new root CA blueprint +// NewCA creates a new root CA blueprint. func NewCA(secretName, secretNamespace string) *CA { return &CA{ SecretName: secretName, @@ -180,7 +176,7 @@ func NewCA(secretName, secretNamespace string) *CA { } } -// loadKeyPair populates c.CACert/c.CAKey from c.CACertBytes/c.CAKeyBytes +// loadKeyPair populates c.CACert/c.CAKey from c.CACertBytes/c.CAKeyBytes. func (c *CA) loadKeyPair() error { caCert, err := helpers.ParseCertificatePEM(c.CACertBytes) if err != nil { @@ -197,25 +193,27 @@ func (c *CA) loadKeyPair() error { return nil } -// LoadedFromSecret returns true if this CA was loaded from a K8s secret +// LoadedFromSecret returns true if this CA was loaded from a K8s secret. func (c *CA) LoadedFromSecret() bool { return c.loadedFromSecret } -// IsEmpty returns true if this CA is empty +// IsEmpty returns true if this CA is empty. func (c *CA) IsEmpty() bool { return c.CAKey == nil && c.CACert == nil } -// Reset resets ca key and ca cert values, this is useful for reload or regeneration. +// Reset resets ca key and ca cert values, this is useful for reload or +// regeneration. func (c *CA) Reset() { c.CAKey = nil c.CACert = nil c.loadedFromSecret = false } -// Generate the root certificate and keyfile. Populates c.CACertBytes and c.CAKeyBytes -func (c *CA) Generate(commonName string, validityDuration time.Duration) error { +// Generate the root certificate and keyfile. Populates c.CACertBytes and +// c.CAKeyBytes. +func (c *CA) Generate(log *slog.Logger, commonName string, validityDuration time.Duration) error { log.Info("Creating CSR for certificate authority", logfields.CertCommonName, commonName, logfields.CertValidityDuration, validityDuration.String(), @@ -239,7 +237,8 @@ func (c *CA) Generate(commonName string, validityDuration time.Duration) error { return c.loadKeyPair() } -// LoadFromFile populates c.CACertBytes and c.CAKeyBytes by reading them from file. +// LoadFromFile populates c.CACertBytes and c.CAKeyBytes by reading them from +// file. func (c *CA) LoadFromFile(caCertFile, caKeyFile string) error { if caCertFile == "" || caKeyFile == "" { return errors.New("path for CA key and cert file must both be provided if CA is not generated") @@ -261,11 +260,12 @@ func (c *CA) LoadFromFile(caCertFile, caKeyFile string) error { return c.loadKeyPair() } -// StoreAsSecret creates or updates the CA certificate in a K8s secret -// - If force is true, the existing secret with same name in same namespace (if available) will be overwritten. -// - If force is false and there is existing secret with same name in same namespace, just -// throws IsAlreadyExists error to caller -func (c *CA) StoreAsSecret(ctx context.Context, k8sClient *kubernetes.Clientset, force bool) error { +// StoreAsSecret creates or updates the CA certificate in a K8s secret. +// - If force is true, the existing secret with same name in same namespace +// (if available) will be overwritten. +// - If force is false and there is existing secret with same name in same +// namespace, just throws IsAlreadyExists error to caller. +func (c *CA) StoreAsSecret(ctx context.Context, log *slog.Logger, k8sClient *kubernetes.Clientset, force bool) error { if c.CACertBytes == nil || c.CAKeyBytes == nil { return fmt.Errorf("cannot create secret %s/%s from empty certificate", c.SecretNamespace, c.SecretName) @@ -302,7 +302,8 @@ func (c *CA) StoreAsSecret(ctx context.Context, k8sClient *kubernetes.Clientset, return err } -// LoadFromSecret populates c.CACertBytes and c.CAKeyBytes by reading them from a secret +// LoadFromSecret populates c.CACertBytes and c.CAKeyBytes by reading them from +// a secret. func (c *CA) LoadFromSecret(ctx context.Context, k8sClient *kubernetes.Clientset) error { k8sSecrets := k8sClient.CoreV1().Secrets(c.SecretNamespace) secret, err := k8sSecrets.Get(ctx, c.SecretName, meta_v1.GetOptions{}) diff --git a/internal/logging/logfields/logfields.go b/internal/logging/logfields/logfields.go index e8f213e4..ebcecc8f 100644 --- a/internal/logging/logfields/logfields.go +++ b/internal/logging/logfields/logfields.go @@ -13,7 +13,7 @@ const ( // CertCommonName is the field denoting a x509 certificate's CN. CertCommonName = "certCommonName" // CertValidityDuration is the field denoting a x509 certificate's validity - // durationg + // duration. CertValidityDuration = "certValidityDuration" // CertUsage is the field denoting a x509 certificate's key usages. CertUsage = "certUsage" diff --git a/internal/logging/logging.go b/internal/logging/logging.go index a8ab4d5e..c3a2ce22 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -12,12 +12,11 @@ import ( cfsslLog "github.com/cloudflare/cfssl/log" ) -// DefaultLoggerLvl is a runtime-configurable log level used by DefaultLogger. +// Level is a runtime-configurable log level used by Logger. var Level = new(slog.LevelVar) -// DefaultLogger is the log/slog logger instance used through the certgen -// packages. -var DefaultLogger = slog.New( +// Logger is the log/slog logger instance used through the certgen packages. +var Logger = slog.New( slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: Level, }), @@ -25,41 +24,41 @@ var DefaultLogger = slog.New( func init() { cfsslLog.SetLogger(&sysLogger{ - l: DefaultLogger.With(logfields.LogSubsys, "cfssl"), + l: Logger.With(logfields.LogSubsys, "cfssl"), //nolint:sloglint }) } -// sysLogger wraps slog to implement the cfsslLog.SyslogWriter +// sysLogger wraps slog to implement the cfsslLog.SyslogWriter. type sysLogger struct { l *slog.Logger } -// Debug implements cfsslLog.SyslogWriter +// Debug implements cfsslLog.SyslogWriter. func (s *sysLogger) Debug(msg string) { s.l.Debug(msg, logfields.LogSyslog, "debug") //nolint:sloglint } -// Info implements cfsslLog.SyslogWriter +// Info implements cfsslLog.SyslogWriter. func (s *sysLogger) Info(msg string) { s.l.Info(msg, logfields.LogSyslog, "info") //nolint:sloglint } -// Warning implements cfsslLog.SyslogWriter +// Warning implements cfsslLog.SyslogWriter. func (s *sysLogger) Warning(msg string) { s.l.Warn(msg, logfields.LogSyslog, "warning") //nolint:sloglint } -// Error implements cfsslLog.SyslogWriter +// Err implements cfsslLog.SyslogWriter. func (s *sysLogger) Err(msg string) { s.l.Error(msg, logfields.LogSyslog, "err") //nolint:sloglint } -// Crit implements cfsslLog.SyslogWriter +// Crit implements cfsslLog.SyslogWriter. func (s *sysLogger) Crit(msg string) { s.l.Error(msg, logfields.LogSyslog, "crit") //nolint:sloglint } -// Emerg implements cfsslLog.SyslogWriter +// Emerg implements cfsslLog.SyslogWriter. func (s *sysLogger) Emerg(msg string) { s.l.Error(msg, logfields.LogSyslog, "emerg") //nolint:sloglint } diff --git a/internal/option/config.go b/internal/option/config.go index 6fd50f63..bba35d39 100644 --- a/internal/option/config.go +++ b/internal/option/config.go @@ -57,7 +57,7 @@ const ( CertsConfigFile = "config-file" ) -// CertGenConfig contains the main configuration options +// CertGenConfig contains the main configuration options. type CertGenConfig struct { // Debug enables debug messages. Debug bool @@ -101,10 +101,12 @@ type CertGenConfig struct { CertsConfigFile string } +// CertificateConfigs contains configuration of individual certificates. type CertificateConfigs struct { Certs []CertificateConfig `yaml:"certs"` } +// CertificateConfig contains the configuration of a certificate. type CertificateConfig struct { Name string `yaml:"name"` Namespace string `yaml:"namespace"` @@ -114,7 +116,7 @@ type CertificateConfig struct { Validity time.Duration `yaml:"validity"` } -// PopulateFrom populates the config struct with the values provided by vp +// PopulateFrom populates the config struct with the values provided by vp. func (c *CertGenConfig) PopulateFrom(vp *viper.Viper) { c.Debug = vp.GetBool(Debug) c.K8sKubeConfigPath = vp.GetString(K8sKubeConfigPath) diff --git a/internal/version/version.go b/internal/version/version.go index 9eb9bfdc..e1497d4a 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -4,4 +4,4 @@ package version // Version is the certgen version string. -var Version = "0.2.4" +var Version = "0.3.0"