diff --git a/Dockerfile b/Dockerfile index ea9114e..f9440c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-07-25T21:33:47Z by kres faf91e3. +# Generated on 2024-08-14T09:22:15Z by kres 7be2a05. ARG TOOLCHAIN @@ -11,7 +11,7 @@ FROM ghcr.io/siderolabs/ca-certificates:v1.7.0 AS image-ca-certificates FROM ghcr.io/siderolabs/fhs:v1.7.0 AS image-fhs # runs markdownlint -FROM docker.io/oven/bun:1.1.20-alpine AS lint-markdown +FROM docker.io/oven/bun:1.1.22-alpine AS lint-markdown WORKDIR /src RUN bun i markdownlint-cli@0.41.0 sentences-per-line@0.2.1 COPY .markdownlint.json . diff --git a/Makefile b/Makefile index 428ef20..2e3d6bb 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-07-25T21:33:47Z by kres faf91e3. +# Generated on 2024-08-14T09:22:15Z by kres 7be2a05. # common variables @@ -18,14 +18,14 @@ REGISTRY ?= ghcr.io USERNAME ?= siderolabs REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME) PROTOBUF_GO_VERSION ?= 1.34.2 -GRPC_GO_VERSION ?= 1.4.0 -GRPC_GATEWAY_VERSION ?= 2.20.0 +GRPC_GO_VERSION ?= 1.5.1 +GRPC_GATEWAY_VERSION ?= 2.21.0 VTPROTOBUF_VERSION ?= 0.6.0 -GOIMPORTS_VERSION ?= 0.23.0 +GOIMPORTS_VERSION ?= 0.24.0 DEEPCOPY_VERSION ?= v0.5.6 -GOLANGCILINT_VERSION ?= v1.59.1 +GOLANGCILINT_VERSION ?= v1.60.1 GOFUMPT_VERSION ?= v0.6.0 -GO_VERSION ?= 1.22.5 +GO_VERSION ?= 1.23.0 GO_BUILDFLAGS ?= GO_LDFLAGS ?= CGO_ENABLED ?= 0 @@ -67,7 +67,7 @@ COMMON_ARGS += --build-arg=DEEPCOPY_VERSION="$(DEEPCOPY_VERSION)" COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)" COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)" COMMON_ARGS += --build-arg=TESTPKGS="$(TESTPKGS)" -TOOLCHAIN ?= docker.io/golang:1.22-alpine +TOOLCHAIN ?= docker.io/golang:1.23-alpine # help menu diff --git a/cmd/kube-service-exposer/main.go b/cmd/kube-service-exposer/main.go index 96431e3..cf4bbe9 100644 --- a/cmd/kube-service-exposer/main.go +++ b/cmd/kube-service-exposer/main.go @@ -6,13 +6,19 @@ package main import ( + "context" + "errors" "fmt" "log" + "net/http" + "net/http/pprof" + "time" "github.com/go-logr/zapr" "github.com/spf13/cobra" "go.uber.org/zap" "go.uber.org/zap/zapcore" + "golang.org/x/sync/errgroup" controllerruntimelog "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/manager/signals" @@ -22,8 +28,10 @@ import ( ) var rootCmdArgs struct { - annotationKey string - bindCIDRs []string + annotationKey string + pprofBindAddr string + bindCIDRs []string + disallowedHostPortRanges []string debug bool } @@ -57,12 +65,24 @@ var rootCmd = &cobra.Command{ controllerruntimelog.SetLogger(zapr.NewLogger(logger)) - exposer, err := exposer.New(rootCmdArgs.annotationKey, rootCmdArgs.bindCIDRs, logger.With(zap.String("component", "exposer"))) + exposer, err := exposer.New(rootCmdArgs.annotationKey, rootCmdArgs.bindCIDRs, rootCmdArgs.disallowedHostPortRanges, logger.With(zap.String("component", "exposer"))) if err != nil { return err } - return exposer.Run(cmd.Context()) + eg, ctx := errgroup.WithContext(cmd.Context()) + + eg.Go(func() error { + return exposer.Run(ctx) + }) + + if rootCmdArgs.pprofBindAddr != "" { + eg.Go(func() error { + return runPprofServer(ctx, logger) + }) + } + + return eg.Wait() }, } @@ -74,10 +94,56 @@ func main() { } } +func runPprofServer(ctx context.Context, logger *zap.Logger) error { + logger.Info("starting pprof server", zap.String("addr", rootCmdArgs.pprofBindAddr)) + + mux := http.NewServeMux() + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + server := &http.Server{ + Addr: rootCmdArgs.pprofBindAddr, + Handler: mux, + } + + errCh := make(chan error, 1) + + go func() { errCh <- server.ListenAndServe() }() + + select { + case err := <-errCh: + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("failed to serve: %w", err) + } + + return nil + case <-ctx.Done(): + } + + logger.Info("stopping pprof server") + + shutdownCtx, shutdownCtxCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer shutdownCtxCancel() + + //nolint:contextcheck + if err := server.Shutdown(shutdownCtx); err != nil { + return fmt.Errorf("failed to shutdown pprof server gracefully: %w", err) + } + + return nil +} + func init() { rootCmd.Flags().StringVarP(&rootCmdArgs.annotationKey, "annotation-key", "a", version.Name+".sidero.dev/port", "the annotation key to be looked for on the services to determine which port to expose ot from.") - rootCmd.Flags().StringSliceVarP(&rootCmdArgs.bindCIDRs, "bind-cidrs", "b", []string{}, + rootCmd.Flags().StringVar(&rootCmdArgs.pprofBindAddr, "pprof-bind-addr", "", + "the address to bind the pprof server to. Disabled when empty.") + rootCmd.Flags().StringSliceVarP(&rootCmdArgs.bindCIDRs, "bind-cidrs", "b", nil, "the CIDRs to match the host IPs with. Only the ports on the IPs that match these CIDRs will be listened. When empty, all IPs will be listened.") + rootCmd.Flags().StringSliceVar(&rootCmdArgs.disallowedHostPortRanges, "disallowed-host-port-ranges", nil, + "the port ranges on the host that are not allowed to be used. When a disallowed host port is attempted to be exposed, it will be skipped and a warning will be logged.") rootCmd.Flags().BoolVar(&rootCmdArgs.debug, "debug", false, "enable debug logs.") } diff --git a/deploy/kube-service-exposer.yaml b/deploy/kube-service-exposer.yaml index b507f40..e8e07f7 100644 --- a/deploy/kube-service-exposer.yaml +++ b/deploy/kube-service-exposer.yaml @@ -47,8 +47,9 @@ spec: - operator: Exists containers: - name: kube-service-exposer - image: ghcr.io/siderolabs/kube-service-exposer:v0.1.0 + image: ghcr.io/siderolabs/kube-service-exposer:v0.2.0 # args: # - --debug=true + # - --pprof-bind-addr=:6060 # - --annotation-key=my-annotation-key/port # - --bind-cidrs=172.20.0.0/24 diff --git a/go.mod b/go.mod index 7fa1820..104b0b9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/siderolabs/kube-service-exposer -go 1.22.3 +go 1.23.0 require ( github.com/benbjohnson/clock v1.3.5 @@ -12,17 +12,18 @@ require ( github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 - golang.org/x/sync v0.7.0 - k8s.io/api v0.30.2 - k8s.io/apimachinery v0.30.2 - k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 - sigs.k8s.io/controller-runtime v0.18.4 + golang.org/x/sync v0.8.0 + k8s.io/api v0.30.3 + k8s.io/apimachinery v0.30.3 + k8s.io/client-go v0.30.3 + k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 + sigs.k8s.io/controller-runtime v0.18.5 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect @@ -48,29 +49,28 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.54.0 // indirect + github.com/prometheus/common v0.55.0 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/siderolabs/tcpproxy v0.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/oauth2 v0.22.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect + golang.org/x/time v0.6.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/client-go v0.30.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a // indirect + k8s.io/kube-openapi v0.0.0-20240812233141-91dab695df6f // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect diff --git a/go.sum b/go.sum index b4780c3..90da967 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,9 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= @@ -44,8 +45,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= -github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8 h1:FKHo8hFI3A+7w0aUQuYXQ+6EN5stWmeY/AZqtM8xk9k= +github.com/google/pprof v0.0.0-20240727154555-813a5fbdbec8/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -76,20 +77,21 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= -github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/ginkgo/v2 v2.20.0 h1:PE84V2mHqoT1sglvHc8ZdQtPcwmvvt29WLEEO3xmdZw= +github.com/onsi/ginkgo/v2 v2.20.0/go.mod h1:lG9ey2Z29hR41WMVthyJBGUBcBhGOtoPF2VFMvBXFCI= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.54.0 h1:ZlZy0BgJhTwVZUn7dLOkwCZHUkrAqd3WYtcFCWnM1D8= -github.com/prometheus/common v0.54.0/go.mod h1:/TQgMJP5CuVYveyT7n/0Ix8yLNNXy9yRSkhnLTHPDIQ= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= @@ -122,43 +124,43 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= -golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -177,22 +179,22 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= -k8s.io/api v0.30.2/go.mod h1:ULg5g9JvOev2dG0u2hig4Z7tQ2hHIuS+m8MNZ+X6EmI= +k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= +k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= -k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg= -k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= -k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50= -k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs= +k8s.io/apimachinery v0.30.3 h1:q1laaWCmrszyQuSQCfNB8cFgCuDAoPszKY4ucAjDwHc= +k8s.io/apimachinery v0.30.3/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.3 h1:bHrJu3xQZNXIi8/MoxYtZBBWQQXwy16zqJwloXXfD3k= +k8s.io/client-go v0.30.3/go.mod h1:8d4pf8vYu665/kUbsxWAQ/JDBNWqfFeZnvFiVdmx89U= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a h1:zD1uj3Jf+mD4zmA7W+goE5TxDkI7OGJjBNBzq5fJtLA= -k8s.io/kube-openapi v0.0.0-20240521193020-835d969ad83a/go.mod h1:UxDHUPsUwTOOxSU+oXURfFBcAS6JwiRXTYqYwfuGowc= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0 h1:jgGTlFYnhF1PM1Ax/lAlxUPE+KfCIXHaathvJg1C3ak= -k8s.io/utils v0.0.0-20240502163921-fe8a2dddb1d0/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.18.4 h1:87+guW1zhvuPLh1PHybKdYFLU0YJp4FhJRmiHvm5BZw= -sigs.k8s.io/controller-runtime v0.18.4/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= +k8s.io/kube-openapi v0.0.0-20240812233141-91dab695df6f h1:bnWtxXWdAl5bVOCEPoNdvMkyj6cTW3zxHuwKIakuV9w= +k8s.io/kube-openapi v0.0.0-20240812233141-91dab695df6f/go.mod h1:G0W3eI9gG219NHRq3h5uQaRBl4pj4ZpwzRP5ti8y770= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A= +k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.18.5 h1:nTHio/W+Q4aBlQMgbnC5hZb4IjIidyrizMai9P6n4Rk= +sigs.k8s.io/controller-runtime v0.18.5/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= diff --git a/internal/exposer/exposer.go b/internal/exposer/exposer.go index ef9ae11..3b380ba 100644 --- a/internal/exposer/exposer.go +++ b/internal/exposer/exposer.go @@ -38,12 +38,12 @@ type Exposer struct { } // New creates a new Exposer. -func New(annotationKey string, bindCIDRs []string, logger *zap.Logger) (*Exposer, error) { +func New(annotationKey string, bindCIDRs, disallowedHostPortRanges []string, logger *zap.Logger) (*Exposer, error) { if logger == nil { logger = zap.NewNop() } - logger = logger.With(zap.String("annotation-key", annotationKey), zap.Strings("bind-cidrs", bindCIDRs)) + logger = logger.With(zap.String("annotation-key", annotationKey), zap.Strings("bind-cidrs", bindCIDRs), zap.Strings("disallowed-host-port-ranges", disallowedHostPortRanges)) ipCollector := ip.NewCollector() @@ -67,12 +67,12 @@ func New(annotationKey string, bindCIDRs []string, logger *zap.Logger) (*Exposer return nil, fmt.Errorf("failed to create ipSetProvider: %w", err) } - ipMapper, err := ip.NewMapper(ipSetProvider, nil, logger.With(zap.String("component", "ip-mapper"))) + ipMapper, err := ip.NewMapper(ipSetProvider, &ip.TCPLoadBalancerProvider{}, logger.With(zap.String("component", "ip-mapper"))) if err != nil { return nil, fmt.Errorf("failed to create ipMapper: %w", err) } - serviceHandler, err := service.NewHandler(annotationKey, ipMapper, logger.With(zap.String("component", "service-handler"))) + serviceHandler, err := service.NewHandler(annotationKey, ipMapper, disallowedHostPortRanges, logger.With(zap.String("component", "service-handler"))) if err != nil { return nil, fmt.Errorf("failed to create serviceHandler: %w", err) } diff --git a/internal/exposer/ip_test.go b/internal/exposer/ip_test.go index 3c9e1a5..4c2bd0d 100644 --- a/internal/exposer/ip_test.go +++ b/internal/exposer/ip_test.go @@ -10,6 +10,7 @@ import ( "github.com/siderolabs/gen/maps" "github.com/siderolabs/gen/xslices" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "github.com/siderolabs/kube-service-exposer/internal/exposer" @@ -47,10 +48,10 @@ func TestFilteringIPSetProviderEmptyCIDRs(t *testing.T) { logger := zaptest.NewLogger(t) filteringProvider, err := exposer.NewFilteringIPSetProvider([]string{}, &provider, logger) - assert.NoError(t, err) + require.NoError(t, err) ips, err := filteringProvider.Get() - assert.NoError(t, err) + require.NoError(t, err) assert.ElementsMatch(t, maps.Keys(ips), []string{"0.0.0.0"}) } @@ -66,18 +67,18 @@ func TestFilteringIPSetProviderFilter(t *testing.T) { logger := zaptest.NewLogger(t) filteringProvider, err := exposer.NewFilteringIPSetProvider([]string{"172.20.0.0/24", "192.168.3.0/24"}, &provider, logger) - assert.NoError(t, err) + require.NoError(t, err) ips, err := filteringProvider.Get() - assert.NoError(t, err) + require.NoError(t, err) assert.ElementsMatch(t, maps.Keys(ips), []string{"172.20.0.42"}) filteringProvider, err = exposer.NewFilteringIPSetProvider([]string{"172.20.0.0/16", "192.168.2.0/24"}, &provider, logger) - assert.NoError(t, err) + require.NoError(t, err) ips, err = filteringProvider.Get() - assert.NoError(t, err) + require.NoError(t, err) assert.ElementsMatch(t, maps.Keys(ips), []string{"172.20.0.42", "192.168.2.42"}) } diff --git a/internal/ip/loadbalancer.go b/internal/ip/loadbalancer.go index 3ea309b..97b4382 100644 --- a/internal/ip/loadbalancer.go +++ b/internal/ip/loadbalancer.go @@ -15,6 +15,7 @@ type LoadBalancer interface { AddRoute(ipPort string, upstreamAddrs []string, options ...upstream.ListOption) error Start() error Close() error + Wait() error } // LoadBalancerProvider is a factory for LoadBalancer instances. @@ -25,6 +26,7 @@ type LoadBalancerProvider interface { // TCPLoadBalancerProvider is a LoadBalancerProvider that creates and returns loadbalancer.TCP instances. type TCPLoadBalancerProvider struct{} +// New returns a new loadbalancer.TCP instance. // New returns a new loadbalancer.TCP instance. func (t *TCPLoadBalancerProvider) New(logger *zap.Logger) (LoadBalancer, error) { if logger == nil { diff --git a/internal/ip/mapper.go b/internal/ip/mapper.go index 0ed6d3b..427cb41 100644 --- a/internal/ip/mapper.go +++ b/internal/ip/mapper.go @@ -5,6 +5,7 @@ package ip import ( + "errors" "fmt" "net" "reflect" @@ -46,10 +47,6 @@ func NewMapper(ipSetProvider SetProvider, loadBalancerController LoadBalancerPro return nil, fmt.Errorf("ipSetProvider must not be nil") } - if loadBalancerController == nil { - loadBalancerController = &TCPLoadBalancerProvider{} - } - if logger == nil { logger = zap.NewNop() } @@ -133,6 +130,13 @@ func (m *Mapper) Add(svcName string, hostPort, svcPort int) error { } if err = lb.Start(); err != nil { + logger.Info("failed to start loadbalancer, attempt to stop it") + + // we still need to close the loadbalancer, so that the health checks goroutines get terminated + if closeErr := lb.Close(); closeErr != nil { + return errors.Join(fmt.Errorf("failed to start loadbalancer: %w", err), fmt.Errorf("failed to close loadbalancer: %w", closeErr)) + } + return fmt.Errorf("failed to start loadbalancer: %w", err) } @@ -165,6 +169,11 @@ func (m *Mapper) removeNoLock(svcName string) { if mapping.lb != nil { if err := mapping.lb.Close(); err != nil { logger.Info("error on closing load balancer", zap.Error(err)) + } else { + // successfully closed, wait for the proxy to finish running + if err = mapping.lb.Wait(); err != nil { + logger.Info("error on waiting for load balancer to close", zap.Error(err)) + } } } diff --git a/internal/ip/mapper_test.go b/internal/ip/mapper_test.go index db3c10c..94cf5a0 100644 --- a/internal/ip/mapper_test.go +++ b/internal/ip/mapper_test.go @@ -10,6 +10,7 @@ import ( "github.com/siderolabs/gen/xslices" "github.com/siderolabs/go-loadbalancer/upstream" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap" "go.uber.org/zap/zaptest" @@ -30,6 +31,10 @@ type mockLoadBalancer struct { closed bool } +func (m *mockLoadBalancer) Wait() error { + return nil +} + func (m *mockLoadBalancer) AddRoute(ipPort string, upstreamAddrs []string, _ ...upstream.ListOption) error { if m.routes == nil { m.routes = make(map[string][]string) @@ -72,8 +77,8 @@ func TestMapperCreate(t *testing.T) { _, err := ip.NewMapper(nil, nil, logger) assert.ErrorContains(t, err, "must not be nil") - mapper, err := ip.NewMapper(&mockIPSetProvider{}, nil, logger) - assert.NoError(t, err) + mapper, err := ip.NewMapper(&mockIPSetProvider{}, &ip.TCPLoadBalancerProvider{}, logger) + require.NoError(t, err) assert.NotNil(t, mapper) } @@ -91,9 +96,9 @@ func TestMapper(t *testing.T) { logger := zaptest.NewLogger(t) mapper, err := ip.NewMapper(ipSetProvider, lbProvider, logger) - assert.NoError(t, err) + require.NoError(t, err) - assert.NoError(t, mapper.Add("svc1.ns1", 12345, 80)) + require.NoError(t, mapper.Add("svc1.ns1", 12345, 80)) assert.Len(t, lbProvider.lbs, 1) assert.Len(t, lbProvider.lbs[0].(*mockLoadBalancer).routes, 2) @@ -102,14 +107,14 @@ func TestMapper(t *testing.T) { assert.ErrorContains(t, mapper.Add("svc2.ns2", 12345, 80), "already registered to another service") - assert.NoError(t, mapper.Add("svc2.ns2", 12346, 8080)) + require.NoError(t, mapper.Add("svc2.ns2", 12346, 8080)) assert.Len(t, lbProvider.lbs, 2) assert.Len(t, lbProvider.lbs[1].(*mockLoadBalancer).routes, 2) assert.Equal(t, []string{"svc2.ns2:8080"}, lbProvider.lbs[1].(*mockLoadBalancer).routes["192.168.2.42:12346"]) assert.Equal(t, []string{"svc2.ns2:8080"}, lbProvider.lbs[1].(*mockLoadBalancer).routes["172.20.0.42:12346"]) - assert.NoError(t, mapper.Add("svc2.ns2", 12347, 8081)) + require.NoError(t, mapper.Add("svc2.ns2", 12347, 8081)) assert.Len(t, lbProvider.lbs, 3) assert.Len(t, lbProvider.lbs[2].(*mockLoadBalancer).routes, 2) diff --git a/internal/ip/tracker_test.go b/internal/ip/tracker_test.go index bc9c0b3..8a63966 100644 --- a/internal/ip/tracker_test.go +++ b/internal/ip/tracker_test.go @@ -15,6 +15,7 @@ import ( "github.com/siderolabs/gen/xslices" "github.com/siderolabs/go-retry/retry" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" corev1 "k8s.io/api/core/v1" @@ -83,7 +84,7 @@ func TestTrackerCreate(t *testing.T) { assert.ErrorContains(t, err, "serviceHandler must not be nil") tracker, err := ip.NewTracker(&mockSetRefresher{}, &mockClientProvider{}, &mockServiceHandler{}, 30*time.Second, nil, logger) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, tracker) } @@ -116,7 +117,7 @@ func TestTracker(t *testing.T) { serviceHandler := &mockServiceHandler{} tracker, err := ip.NewTracker(refresher, clientProvider, serviceHandler, 2*time.Second, mockClock, logger) - assert.NoError(t, err) + require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -152,7 +153,7 @@ func TestTracker(t *testing.T) { return nil }) - assert.NoError(t, err) + require.NoError(t, err) assert.ElementsMatch(t, xslices.Map(serviceHandler.Handles(), func(svc *corev1.Service) string { return svc.Name + "." + svc.Namespace @@ -167,7 +168,7 @@ func TestTracker(t *testing.T) { cancel() - assert.NoError(t, eg.Wait()) + require.NoError(t, eg.Wait()) } func sleepWithContext(ctx context.Context, d time.Duration) { diff --git a/internal/memoizer/memoizer_test.go b/internal/memoizer/memoizer_test.go index fd57578..98f8f39 100644 --- a/internal/memoizer/memoizer_test.go +++ b/internal/memoizer/memoizer_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/siderolabs/kube-service-exposer/internal/memoizer" ) @@ -28,16 +29,16 @@ func TestGet(t *testing.T) { return "aaa", nil }) - assert.NoError(t, err) + require.NoError(t, err) val, err := m.Get() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 1, called) assert.Equal(t, "aaa", val) val, err = m.Get() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 1, called) assert.Equal(t, "aaa", val) @@ -61,20 +62,20 @@ func TestRefresh(t *testing.T) { return "", fmt.Errorf("unexpected call") }) - assert.NoError(t, err) + require.NoError(t, err) val, err := m.Get() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 1, called) assert.Equal(t, "aaa", val) newVal, err := m.Refresh() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "bbb", newVal) newVal, err = m.Refresh() - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "bbb", newVal) _, err = m.Refresh() diff --git a/internal/reconciler/reconciler_test.go b/internal/reconciler/reconciler_test.go index 8b9e952..80c390c 100644 --- a/internal/reconciler/reconciler_test.go +++ b/internal/reconciler/reconciler_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -54,7 +55,7 @@ func TestReconcilerCreate(t *testing.T) { assert.ErrorContains(t, err, "serviceHandler must not be nil") rec, err := reconciler.New(&mockClientProvider{}, &mockServiceHandler{}) - assert.NoError(t, err) + require.NoError(t, err) assert.NotNil(t, rec) } @@ -75,7 +76,7 @@ func TestReconcilerReconcile(t *testing.T) { serviceHandler := &mockServiceHandler{} rec, err := reconciler.New(clientProvider, serviceHandler) - assert.NoError(t, err) + require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), 3) defer cancel() @@ -86,7 +87,7 @@ func TestReconcilerReconcile(t *testing.T) { Namespace: "testns", }, }) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, serviceHandler.deletes, 0) @@ -99,7 +100,7 @@ func TestReconcilerReconcile(t *testing.T) { Namespace: "testns", }, }) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, serviceHandler.deletes[0], "non-existent.testns") assert.Len(t, serviceHandler.handles, 1) diff --git a/internal/service/handler.go b/internal/service/handler.go index a9a307f..e1a8f0c 100644 --- a/internal/service/handler.go +++ b/internal/service/handler.go @@ -12,6 +12,7 @@ import ( "go.uber.org/zap" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/validation" + "k8s.io/apimachinery/pkg/util/net" "k8s.io/apimachinery/pkg/util/validation/field" ) @@ -23,17 +24,29 @@ type IPMapper interface { // Handler is a handler for services. type Handler struct { - ipMapper IPMapper - logger *zap.Logger - annotationKey string + ipMapper IPMapper + logger *zap.Logger + annotationKey string + disallowedPortRanges []*net.PortRange } // NewHandler returns a new Handler. -func NewHandler(annotationKey string, ipMapper IPMapper, logger *zap.Logger) (*Handler, error) { +func NewHandler(annotationKey string, ipMapper IPMapper, disallowedHostPortRanges []string, logger *zap.Logger) (*Handler, error) { if logger == nil { logger = zap.NewNop() } + portRanges := make([]*net.PortRange, 0, len(disallowedHostPortRanges)) + + for _, disallowedHostPort := range disallowedHostPortRanges { + portRange, err := net.ParsePortRange(disallowedHostPort) + if err != nil { + return nil, fmt.Errorf("invalid port range %q: %w", disallowedHostPort, err) + } + + portRanges = append(portRanges, portRange) + } + errs := validation.ValidateAnnotations(map[string]string{annotationKey: "65535"}, field.NewPath("metadata", "annotations")) if len(errs) > 0 { return nil, fmt.Errorf("invalid annotation key: %w", errs.ToAggregate()) @@ -44,9 +57,10 @@ func NewHandler(annotationKey string, ipMapper IPMapper, logger *zap.Logger) (*H } return &Handler{ - annotationKey: annotationKey, - ipMapper: ipMapper, - logger: logger, + annotationKey: annotationKey, + ipMapper: ipMapper, + disallowedPortRanges: portRanges, + logger: logger, }, nil } @@ -89,6 +103,16 @@ func (s *Handler) Handle(svc *corev1.Service) error { return fmt.Errorf("invalid host port %q: %w", hostPortStr, err) } + for _, portRange := range s.disallowedPortRanges { + if portRange.Contains(hostPort) { + logger.Warn("disallowed host port", zap.Int("host-port", hostPort), zap.String("disallowed-port-range", portRange.String())) + + s.ipMapper.Remove(svcName) + + return nil + } + } + svcTCPPorts := xslices.Filter(svc.Spec.Ports, func(port corev1.ServicePort) bool { return port.Protocol == corev1.ProtocolTCP }) diff --git a/internal/service/handler_test.go b/internal/service/handler_test.go index 7ed91cb..f442cf6 100644 --- a/internal/service/handler_test.go +++ b/internal/service/handler_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -45,17 +46,17 @@ func TestHandlerCreate(t *testing.T) { logger := zaptest.NewLogger(t) - _, err := service.NewHandler("test", nil, logger) + _, err := service.NewHandler("test", nil, nil, logger) assert.ErrorContains(t, err, "must not be nil") - _, err = service.NewHandler("", &mockIPMapper{}, logger) + _, err = service.NewHandler("", &mockIPMapper{}, nil, logger) assert.ErrorContains(t, err, "invalid annotation key") - _, err = service.NewHandler("invalid key 1", &mockIPMapper{}, logger) + _, err = service.NewHandler("invalid key 1", &mockIPMapper{}, nil, logger) assert.ErrorContains(t, err, "invalid annotation key") - handler, err := service.NewHandler("valid-key", &mockIPMapper{}, logger) - assert.NoError(t, err) + handler, err := service.NewHandler("valid-key", &mockIPMapper{}, nil, logger) + require.NoError(t, err) assert.NotNil(t, handler) } @@ -66,8 +67,8 @@ func TestHandlerHandle(t *testing.T) { mapper := mockIPMapper{} - handler, err := service.NewHandler("test", &mapper, logger) - assert.NoError(t, err) + handler, err := service.NewHandler("test", &mapper, nil, logger) + require.NoError(t, err) assert.ErrorContains(t, handler.Handle(nil), "must not be nil") @@ -78,9 +79,9 @@ func TestHandlerHandle(t *testing.T) { }, } - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) - // mo mappings expected, as no ports are defined + // no mappings expected, as no ports are defined assert.Empty(t, mapper.addEntries) tcpPort1 := corev1.ServicePort{ @@ -103,7 +104,7 @@ func TestHandlerHandle(t *testing.T) { svc.Spec.Ports = []corev1.ServicePort{tcpPort1} - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) // no mappings expected, as the service does not have the annotation assert.Empty(t, mapper.addEntries) @@ -118,14 +119,14 @@ func TestHandlerHandle(t *testing.T) { svc.Annotations["test"] = "12345" svc.Spec.Ports = []corev1.ServicePort{udpPort} - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) // no mappings expected, as the service does not contain a TCP port assert.Empty(t, mapper.addEntries) svc.Spec.Ports = []corev1.ServicePort{tcpPort1, udpPort, tcpPort2} - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) // 1 mapping expected for the 1st tcp port assert.Len(t, mapper.addEntries, 1) @@ -136,7 +137,7 @@ func TestHandlerHandle(t *testing.T) { // update annotations svc.Annotations["test"] = "12346" - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) // mapping update expected assert.Len(t, mapper.addEntries, 2) @@ -149,7 +150,7 @@ func TestHandlerHandle(t *testing.T) { removeEntryCount := len(mapper.removeEntries) - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) // mapping removal expected assert.Len(t, mapper.removeEntries, removeEntryCount+1) @@ -158,14 +159,14 @@ func TestHandlerHandle(t *testing.T) { // add annotation again svc.Annotations["test"] = "12347" - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) // remove tcp ports svc.Spec.Ports = []corev1.ServicePort{udpPort} removeEntryCount = len(mapper.removeEntries) - assert.NoError(t, handler.Handle(svc)) + require.NoError(t, handler.Handle(svc)) // mapping removal expected assert.Len(t, mapper.removeEntries, removeEntryCount+1) @@ -179,13 +180,80 @@ func TestHandlerHandleDelete(t *testing.T) { mapper := mockIPMapper{} - handler, err := service.NewHandler("test", &mapper, logger) - assert.NoError(t, err) + handler, err := service.NewHandler("test", &mapper, nil, logger) + require.NoError(t, err) assert.ErrorContains(t, handler.HandleDelete(""), "must not be empty") - assert.NoError(t, handler.HandleDelete("testname.testns")) + require.NoError(t, handler.HandleDelete("testname.testns")) assert.Len(t, mapper.removeEntries, 1) assert.Equal(t, "testname.testns", mapper.removeEntries[0]) } + +func TestHandlerHandleIgnoreDisallowed(t *testing.T) { + t.Parallel() + + logger := zaptest.NewLogger(t) + + mapper := mockIPMapper{} + + handler, err := service.NewHandler("test", &mapper, []string{"0-1024", "10250", "50000"}, logger) + require.NoError(t, err) + + assert.ErrorContains(t, handler.Handle(nil), "must not be nil") + + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testname", + Namespace: "testns", + Annotations: map[string]string{ + "test": "12345", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "tcp-1", + Port: 8080, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + } + + require.NoError(t, handler.Handle(svc)) + + // 1 mapping expected for the 1st tcp port + assert.Len(t, mapper.addEntries, 1) + assert.Equal(t, "testname.testns", mapper.addEntries[0].svcName) + assert.Equal(t, 12345, mapper.addEntries[0].hostPort) + assert.Equal(t, 8080, mapper.addEntries[0].svcPort) + + // update annotations + svc.Annotations["test"] = "1023" + + require.NoError(t, handler.Handle(svc)) + + // mapping removal expected + assert.Len(t, mapper.removeEntries, 1) + + // no new mappings expected + assert.Len(t, mapper.addEntries, 1) + + // update annotations + svc.Annotations["test"] = "50000" + + require.NoError(t, handler.Handle(svc)) + + // no new mappings expected + assert.Len(t, mapper.addEntries, 1) + + // update annotations + svc.Annotations["test"] = "50002" + + require.NoError(t, handler.Handle(svc)) + + // 1 mapping expected for the 1st tcp port + assert.Len(t, mapper.addEntries, 2) +} diff --git a/internal/version/data/sha b/internal/version/data/sha new file mode 100644 index 0000000..66dc905 --- /dev/null +++ b/internal/version/data/sha @@ -0,0 +1 @@ +undefined \ No newline at end of file diff --git a/internal/version/data/tag b/internal/version/data/tag new file mode 100644 index 0000000..7a5e833 --- /dev/null +++ b/internal/version/data/tag @@ -0,0 +1 @@ +v0.1.4 \ No newline at end of file diff --git a/internal/version/version.go b/internal/version/version.go index 3f1426c..12f3ce5 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,14 +2,40 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. -// Package version provides version information. +// THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. +// +// Generated on 2024-08-07T14:11:10Z by kres dbf015a. + +// Package version contains variables such as project name, tag and sha. It's a proper alternative to using +// -ldflags '-X ...'. package version +import ( + _ "embed" + "runtime/debug" + "strings" +) + var ( - // Name is set at build time. - Name string - // Tag is set at build time. + // Tag declares project git tag. + //go:embed data/tag Tag string - // SHA is set at build time. + // SHA declares project git SHA. + //go:embed data/sha SHA string + // Name declares project name. + Name = func() string { + info, ok := debug.ReadBuildInfo() + if !ok { + panic("cannot read build info, something is very wrong") + } + + // Check if siderolabs project + if strings.HasPrefix(info.Path, "github.com/siderolabs/") { + return info.Path[strings.LastIndex(info.Path, "/")+1:] + } + + // We could return a proper full path here, but it could be seen as a privacy violation. + return "community-project" + }() )