From d22746b8e549c25bd3a3cf32de4d2fec6b41b25a Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 12:46:40 +0300 Subject: [PATCH 01/19] feat: implemented logic for domains-go --- Dockerfile | 4 +- Makefile | 6 +- README.md | 7 +- cmd/webhook/cmd/root.go | 90 ++--- cmd/webhook/main.go | 2 +- go.mod | 18 +- go.sum | 36 +- internal/selprovider/apply_changes.go | 265 ++++++++++++ internal/selprovider/apply_changes_test.go | 377 ++++++++++++++++++ internal/selprovider/config.go | 19 + internal/selprovider/domain_filter.go | 7 + internal/selprovider/domain_filter_test.go | 21 + internal/selprovider/helper.go | 86 ++++ internal/selprovider/helper_test.go | 183 +++++++++ .../selprovider/mock/keystone_provider.go | 54 +++ internal/selprovider/models.go | 21 + internal/selprovider/provider.go | 53 +++ internal/selprovider/records.go | 111 ++++++ internal/selprovider/records_test.go | 268 +++++++++++++ internal/selprovider/rrset_fetcher.go | 90 +++++ internal/selprovider/zone_fetcher.go | 75 ++++ pkg/api/adjust_endpoints_test.go | 4 +- pkg/api/api.go | 4 +- pkg/api/api_test.go | 4 +- pkg/api/apply_changes_test.go | 4 +- pkg/api/domain_filter_test.go | 4 +- pkg/api/health_test.go | 4 +- pkg/api/metrics.go | 2 +- pkg/api/metrics_test.go | 6 +- pkg/api/mock/api.go | 19 +- pkg/api/records_test.go | 4 +- pkg/httpclient/default.go | 54 +++ pkg/keystone/provider.go | 72 ++++ pkg/metrics/mock/http_middleware.go | 13 +- 34 files changed, 1879 insertions(+), 108 deletions(-) create mode 100644 internal/selprovider/apply_changes.go create mode 100644 internal/selprovider/apply_changes_test.go create mode 100644 internal/selprovider/config.go create mode 100644 internal/selprovider/domain_filter.go create mode 100644 internal/selprovider/domain_filter_test.go create mode 100644 internal/selprovider/helper.go create mode 100644 internal/selprovider/helper_test.go create mode 100644 internal/selprovider/mock/keystone_provider.go create mode 100644 internal/selprovider/models.go create mode 100644 internal/selprovider/provider.go create mode 100644 internal/selprovider/records.go create mode 100644 internal/selprovider/records_test.go create mode 100644 internal/selprovider/rrset_fetcher.go create mode 100644 internal/selprovider/zone_fetcher.go create mode 100644 pkg/httpclient/default.go create mode 100644 pkg/keystone/provider.go diff --git a/Dockerfile b/Dockerfile index 602ced9..2be6e95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM gcr.io/distroless/static-debian11:nonroot -COPY external-dns-stackit-webhook /external-dns-stackit-webhook +COPY external-dns-webhook /external-dns-webhook -ENTRYPOINT ["/external-dns-stackit-webhook"] +ENTRYPOINT ["/external-dns-webhook"] diff --git a/Makefile b/Makefile index 30d5f4b..e191246 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ GOLANGCI_VERSION = 1.53.3 LICENCES_IGNORE_LIST = $(shell cat licenses/licenses-ignore-list.txt) VERSION ?= 0.0.1 -IMAGE_TAG_BASE ?= stackitcloud/external-dns-stackit-webhook -IMG ?= $(IMAGE_TAG_BASE):$(VERSION) +IMAGE_TAG_BASE ?= selectel/external-dns-webhook +IMG ?= $(IMAGE_TAG_BASE) BUILD_VERSION ?= $(shell git branch --show-current) BUILD_COMMIT ?= $(shell git rev-parse --short HEAD) @@ -17,7 +17,7 @@ download: .PHONY: build build: - CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/external-dns-stackit-webhook -v cmd/webhook/main.go + CGO_ENABLED=0 go build -ldflags "-s -w" -o ./external-dns-webhook -v cmd/webhook/main.go .PHONY: docker-build docker-build: diff --git a/README.md b/README.md index 412a8fa..a8839f9 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,7 @@ demonstrates the deployment as a [sidecar container](https://kubernetes.io/docs/concepts/workloads/pods/#workload-resources-for-managing-pods) within the ExternalDNS pod. -```shell -# We create a Secret from an auth token. Alternatively, you can also -# use keys to authenticate the webhook - see "Authentication" below. +```shell kubectl create secret generic external-dns-stackit-webhook --from-literal=auth-token='' ``` @@ -219,8 +217,7 @@ The configuration of the STACKIT webhook can be accomplished through command lin Below are the options that are available. - `--project-id`/`PROJECT_ID` (required): Specifies the project id of the STACKIT project. -- `--auth-token`/`AUTH_TOKEN` (required if `auth-key-path` is not set): Defines the authentication token for the STACKIT API. Mutually exclusive with 'auth-key-path'. -- `--auth-key-path`/`AUTH_KEY_PATH` (required if `auth-token` is not set): Defines the file path of the service account key for the STACKIT API. Mutually exclusive with 'auth-token'. +- `--auth-token`/`AUTH_TOKEN` (required): Defines the authentication token for the STACKIT API. - `--worker`/`WORKER` (optional): Specifies the number of workers to employ for querying the API. Given that we need to iterate over all zones and records, it can be parallelized. However, it is important to avoid setting this number excessively high to prevent receiving 429 rate limiting from the API (default 10). diff --git a/cmd/webhook/cmd/root.go b/cmd/webhook/cmd/root.go index 71b2042..afc390d 100644 --- a/cmd/webhook/cmd/root.go +++ b/cmd/webhook/cmd/root.go @@ -5,67 +5,62 @@ import ( "log" "strings" + "github.com/selectel/external-dns-webhook/internal/selprovider" + "github.com/selectel/external-dns-webhook/pkg/api" + "github.com/selectel/external-dns-webhook/pkg/keystone" + "github.com/selectel/external-dns-webhook/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" - "github.com/stackitcloud/external-dns-stackit-webhook/internal/stackitprovider" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/metrics" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/stackit" "go.uber.org/zap" "go.uber.org/zap/zapcore" "sigs.k8s.io/external-dns/endpoint" ) var ( - apiPort string - authBearerToken string - authKeyPath string - baseUrl string - projectID string - worker int - domainFilter []string - dryRun bool - logLevel string + authorizationURL string + accountID string + username string + password string + projectID string + apiPort string + baseURL string + worker int + domainFilter []string + dryRun bool + logLevel string ) var rootCmd = &cobra.Command{ - Use: "external-dns-stackit-webhook", - Short: "provider webhook for the STACKIT DNS service", - Long: "provider webhook for the STACKIT DNS service", + Use: "external-dns-selectel-webhook", + Short: "provider webhook for the Selectel DNS service", + Long: "provider webhook for the Selectel DNS service", Run: func(cmd *cobra.Command, args []string) { logger := getLogger() - defer func(logger *zap.Logger) { - err := logger.Sync() - if err != nil { - log.Println(err) - } - }(logger) + defer logger.Sync() endpointDomainFilter := endpoint.DomainFilter{Filters: domainFilter} - stackitConfigOptions, err := stackit.SetConfigOptions(baseUrl, authBearerToken, authKeyPath) - if err != nil { - panic(err) - } - - stackitProvider, err := stackitprovider.NewStackitDNSProvider( - logger.With(zap.String("component", "stackitprovider")), - // ExternalDNS provider config - stackitprovider.Config{ - ProjectId: projectID, - DomainFilter: endpointDomainFilter, - DryRun: dryRun, - Workers: worker, - }, - // STACKIT client SDK config - stackitConfigOptions..., - ) + keystoneProvider := keystone.NewProvider(logger, keystone.Credentials{ + IdentityEndpoint: authorizationURL, + AccountID: accountID, + ProjectID: projectID, + Username: username, + Password: password, + }) + + selProvider, err := selprovider.New(selprovider.Config{ + BaseURL: baseURL, + KeystoneProvider: keystoneProvider, + DomainFilter: endpointDomainFilter, + DryRun: dryRun, + Workers: worker, + }, logger.With(zap.String("component", "selprovider"))) if err != nil { panic(err) } - app := api.New(logger.With(zap.String("component", "api")), metrics.NewHttpApiMetrics(), stackitProvider) + app := api.New(logger.With(zap.String("component", "api")), metrics.NewHttpApiMetrics(), selProvider) err = app.Listen(apiPort) if err != nil { panic(err) @@ -78,6 +73,7 @@ func getLogger() *zap.Logger { Level: zap.NewAtomicLevelAt(getZapLogLevel()), Encoding: "json", // or "console" // ... other zap configuration as needed + EncoderConfig: zap.NewProductionEncoderConfig(), OutputPaths: []string{"stdout"}, ErrorOutputPaths: []string{"stderr"}, } @@ -113,17 +109,19 @@ func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&apiPort, "api-port", "8888", "Specifies the port to listen on.") - rootCmd.PersistentFlags().StringVar(&authBearerToken, "auth-token", "", "Defines the authentication token for the STACKIT API. Mutually exclusive with 'auth-key-path'.") - rootCmd.PersistentFlags().StringVar(&authKeyPath, "auth-key-path", "", "Defines the file path of the service account key for the STACKIT API. Mutually exclusive with 'auth-token'.") - rootCmd.PersistentFlags().StringVar(&baseUrl, "base-url", "https://dns.api.stackit.cloud", " Identifies the Base URL for utilizing the API.") - rootCmd.PersistentFlags().StringVar(&projectID, "project-id", "", "Specifies the project id of the STACKIT project.") + rootCmd.PersistentFlags().StringVar(&baseURL, "base-url", "https://api.selectel.ru/domains/v2", "Identifies the Base URL for utilizing the API.") + rootCmd.PersistentFlags().StringVar(&projectID, "project-id", "", "Specifies the project id to authorize.") + rootCmd.PersistentFlags().StringVar(&accountID, "account-id", "", "Specifies the account id to authorize.") + rootCmd.PersistentFlags().StringVar(&authorizationURL, "auth-url", "https://cloud.api.selcloud.ru/identity/v3", "Identifies the URL for utilizing the API to receive keystone-token.") + rootCmd.PersistentFlags().StringVar(&username, "username", "", "Specifies the username of service user to authorize.") + rootCmd.PersistentFlags().StringVar(&password, "password", "", "Specifies the password of service user to authorize.") rootCmd.PersistentFlags().IntVar(&worker, "worker", 10, "Specifies the number "+ "of workers to employ for querying the API. Given that we need to iterate over all zones and "+ "records, it can be parallelized. However, it is important to avoid setting this number "+ "excessively high to prevent receiving 429 rate limiting from the API.") - rootCmd.PersistentFlags().StringArrayVar(&domainFilter, "domain-filter", []string{}, "Establishes a filter for DNS zone names") + rootCmd.PersistentFlags().StringArrayVar(&domainFilter, "domain-filter", []string{}, "Establishes a filter for DNS zone names.") rootCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "Specifies whether to perform a dry run.") - rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Specifies the log level. Possible values are: debug, info, warn, error") + rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Specifies the log level. Possible values are: debug, info, warn, error.") } func initConfig() { diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 194411f..0ba2ba2 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -1,6 +1,6 @@ package main -import "github.com/stackitcloud/external-dns-stackit-webhook/cmd/webhook/cmd" +import "github.com/selectel/external-dns-webhook/cmd/webhook/cmd" func main() { err := cmd.Execute() diff --git a/go.mod b/go.mod index 2879129..7290a0c 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,18 @@ -module github.com/stackitcloud/external-dns-stackit-webhook +module github.com/selectel/external-dns-webhook go 1.20 require ( github.com/goccy/go-json v0.10.2 github.com/gofiber/adaptor/v2 v2.2.1 - github.com/gofiber/fiber/v2 v2.50.0 + github.com/gofiber/fiber/v2 v2.52.2 + github.com/gophercloud/gophercloud v1.11.0 github.com/prometheus/client_golang v1.17.0 + github.com/selectel/domains-go v1.0.2 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.17.0 - github.com/stackitcloud/stackit-sdk-go/core v0.10.0 + github.com/stackitcloud/stackit-sdk-go/core v0.10.1 github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4 github.com/stretchr/testify v1.8.4 go.uber.org/mock v0.3.0 @@ -27,7 +29,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect @@ -39,7 +41,7 @@ require ( github.com/klauspost/compress v1.17.1 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -60,14 +62,14 @@ require ( github.com/spf13/cast v1.5.1 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.50.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.13.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 9be4b2e..9af9c75 100644 --- a/go.sum +++ b/go.sum @@ -78,12 +78,12 @@ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/gofiber/adaptor/v2 v2.2.1 h1:givE7iViQWlsTR4Jh7tB4iXzrlKBgiraB/yTdHs9Lv4= github.com/gofiber/adaptor/v2 v2.2.1/go.mod h1:AhR16dEqs25W2FY/l8gSj1b51Azg5dtPDmm+pruNOrc= -github.com/gofiber/fiber/v2 v2.50.0 h1:ia0JaB+uw3GpNSCR5nvC5dsaxXjRU5OEu36aytx+zGw= -github.com/gofiber/fiber/v2 v2.50.0/go.mod h1:21eytvay9Is7S6z+OgPi7c7n4++tnClWmhpimVHMimw= +github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDaxEo= +github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= -github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -150,6 +150,10 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gophercloud/gophercloud v1.5.0 h1:cDN6XFCLKiiqvYpjQLq9AiM7RDRbIC9450WpPH+yvXo= +github.com/gophercloud/gophercloud v1.5.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= +github.com/gophercloud/gophercloud v1.11.0 h1:ls0O747DIq1D8SUHc7r2vI8BFbMLeLFuENaAIfEx7OM= +github.com/gophercloud/gophercloud v1.11.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -158,6 +162,7 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -181,8 +186,8 @@ github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3v github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= @@ -221,6 +226,8 @@ github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9c github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/selectel/domains-go v1.0.2 h1:Si6iGaMnTFJxwiJVI50DOdZnwcxc87kqaWrVQYW0a4U= +github.com/selectel/domains-go v1.0.2/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= @@ -235,8 +242,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stackitcloud/stackit-sdk-go/core v0.10.0 h1:IcY8xa/6wo8EhRE9mpCvz4EtTkkoiIa2ZwPHuc5zGyw= -github.com/stackitcloud/stackit-sdk-go/core v0.10.0/go.mod h1:B5dkVm2HlBRG7liBVIFNqncDb6TUHnJ7t0GsKhAFuRk= +github.com/stackitcloud/stackit-sdk-go/core v0.10.1 h1:lzyualywD/2xIsYUHwlqCurG1OwlqCJVtJbOcPO6OzE= +github.com/stackitcloud/stackit-sdk-go/core v0.10.1/go.mod h1:mDX1mSTsB3mP+tNBGcFNx6gH1mGBN4T+dVt+lcw7nlw= github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4 h1:n/X2pVdETDXGHk+vCsg0p3b2zGxSRMJ065to/aAoncg= github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4/go.mod h1:PvgUVFLgELRADWk2epZdCryk0fs8b4DN47ghEJjNWhk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -255,8 +262,8 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.50.0 h1:H7fweIlBm0rXLs2q0XbalvJ6r0CUPFWK3/bB4N13e9M= -github.com/valyala/fasthttp v1.50.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -285,6 +292,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -419,8 +427,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -582,8 +590,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/selprovider/apply_changes.go b/internal/selprovider/apply_changes.go new file mode 100644 index 0000000..b7829d0 --- /dev/null +++ b/internal/selprovider/apply_changes.go @@ -0,0 +1,265 @@ +package selprovider + +import ( + "context" + "fmt" + + domains "github.com/selectel/domains-go/pkg/v2" + "go.uber.org/zap" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" +) + +// ApplyChanges applies a given set of changes in a given zone. +func (p *Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + client, err := p.getDomainsClient() + if err != nil { + return err + } + + // create rr set. POST /zones/{zoneId}/rrset + err = p.createRRSets(ctx, client, changes.Create) + if err != nil { + return err + } + + // update rr set. PATCH /zones/{zoneId}/rrset/{rrSetId} + err = p.updateRRSets(ctx, client, changes.UpdateNew) + if err != nil { + return err + } + + // delete rr set. DELETE /zones/{zoneId}/rrset/{rrSetId} + err = p.deleteRRSets(ctx, client, changes.Delete) + if err != nil { + return err + } + + return nil +} + +// createRRSets creates new record sets for the given endpoints that are in the creation field. +func (p *Provider) createRRSets( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + endpoints []*endpoint.Endpoint, +) error { + if len(endpoints) == 0 { + return nil + } + + zones, err := p.zoneFetcherClient.zones(ctx, client) + if err != nil { + return err + } + + return p.handleRRSetWithWorkers(ctx, client, endpoints, zones, CREATE) +} + +// createRRSet creates a new record set for the given endpoint. +func (p *Provider) createRRSet( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + change *endpoint.Endpoint, + zones []*domains.Zone, +) error { + resultZone, found := findBestMatchingZone(change.DNSName, zones) + if !found { + return fmt.Errorf("no matching zone found for %s", change.DNSName) + } + + logFields := getLogFields(change, CREATE, resultZone.ID) + p.logger.Info("create record set", logFields...) + + if p.dryRun { + p.logger.Debug("dry run, skipping", logFields...) + + return nil + } + + modifyChange(change) + + rrSet := getRRSetRecord(change) + + // ignore all errors to just retry on next run + _, err := client.CreateRRSet(ctx, resultZone.ID, rrSet) + if err != nil { + p.logger.Error("error creating record set", zap.Error(err)) + + return err + } + + p.logger.Info("create record set successfully", logFields...) + + return nil +} + +// updateRRSets patches (overrides) contents in the record sets for the given endpoints that are in the update new field. +func (p *Provider) updateRRSets( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + endpoints []*endpoint.Endpoint, +) error { + if len(endpoints) == 0 { + return nil + } + + zones, err := p.zoneFetcherClient.zones(ctx, client) + if err != nil { + return err + } + + return p.handleRRSetWithWorkers(ctx, client, endpoints, zones, UPDATE) +} + +// updateRRSet patches (overrides) contents in the record set. +func (p *Provider) updateRRSet( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + change *endpoint.Endpoint, + zones []*domains.Zone, +) error { + modifyChange(change) + + resultZone, resultRRSet, err := p.rrSetFetcherClient.getRRSetForUpdateDeletion(ctx, client, change, zones) + if err != nil { + return err + } + + logFields := getLogFields(change, UPDATE, resultRRSet.ID) + p.logger.Info("update record set", logFields...) + + if p.dryRun { + p.logger.Debug("dry run, skipping", logFields...) + + return nil + } + + rrSet := getRRSetRecord(change) + + err = client.UpdateRRSet(ctx, resultZone.ID, resultRRSet.ID, rrSet) + if err != nil { + p.logger.Error("error updating record set", zap.Error(err)) + + return err + } + + p.logger.Info("record set updated successfully", logFields...) + + return nil +} + +// deleteRRSets delete record sets for the given endpoints that are in the deletion field. +func (p *Provider) deleteRRSets( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + endpoints []*endpoint.Endpoint, +) error { + if len(endpoints) == 0 { + p.logger.Debug("no endpoints to delete") + + return nil + } + + p.logger.Info("records to delete", zap.String("records", fmt.Sprintf("%v", endpoints))) + + zones, err := p.zoneFetcherClient.zones(ctx, client) + if err != nil { + return err + } + + return p.handleRRSetWithWorkers(ctx, client, endpoints, zones, DELETE) +} + +// deleteRRSet deletes a record set for the given endpoint. +func (p *Provider) deleteRRSet( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + change *endpoint.Endpoint, + zones []*domains.Zone, +) error { + modifyChange(change) + + resultZone, resultRRSet, err := p.rrSetFetcherClient.getRRSetForUpdateDeletion(ctx, client, change, zones) + if err != nil { + return err + } + + logFields := getLogFields(change, DELETE, resultRRSet.ID) + p.logger.Info("delete record set", logFields...) + + if p.dryRun { + p.logger.Debug("dry run, skipping", logFields...) + + return nil + } + + err = client.DeleteRRSet(ctx, resultZone.ID, resultRRSet.ID) + if err != nil { + p.logger.Error("error deleting record set", zap.Error(err)) + + return err + } + + p.logger.Info("delete record set successfully", logFields...) + + return nil +} + +// handleRRSetWithWorkers handles the given endpoints with workers to optimize speed. +func (p *Provider) handleRRSetWithWorkers( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + endpoints []*endpoint.Endpoint, + zones []*domains.Zone, + action string, +) error { + workerChannel := make(chan changeTask, len(endpoints)) + defer close(workerChannel) + errorChannel := make(chan error, len(endpoints)) + + for i := 0; i < p.workers; i++ { + go p.changeWorker(ctx, client, workerChannel, errorChannel, zones) + } + + for _, change := range endpoints { + workerChannel <- changeTask{ + action: action, + change: change, + } + } + + for i := 0; i < len(endpoints); i++ { + err := <-errorChannel + if err != nil { + return err + } + } + + return nil +} + +// changeWorker is a worker that handles changes passed by a channel. +func (p *Provider) changeWorker( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + changes chan changeTask, + errorChannel chan error, + zones []*domains.Zone, +) { + for change := range changes { + switch change.action { + case CREATE: + err := p.createRRSet(ctx, client, change.change, zones) + errorChannel <- err + case UPDATE: + err := p.updateRRSet(ctx, client, change.change, zones) + errorChannel <- err + case DELETE: + err := p.deleteRRSet(ctx, client, change.change, zones) + errorChannel <- err + } + } + + p.logger.Debug("change worker finished") +} diff --git a/internal/selprovider/apply_changes_test.go b/internal/selprovider/apply_changes_test.go new file mode 100644 index 0000000..0e4045b --- /dev/null +++ b/internal/selprovider/apply_changes_test.go @@ -0,0 +1,377 @@ +package selprovider + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + domains "github.com/selectel/domains-go/pkg/v2" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/plan" +) + +type ChangeType int + +const ( + Create ChangeType = iota + Update + Delete +) + +func TestApplyChanges(t *testing.T) { + t.Parallel() + + testingData := []struct { + changeType ChangeType + }{ + {changeType: Create}, + {changeType: Update}, + {changeType: Delete}, + } + + for _, data := range testingData { + testApplyChanges(t, data.changeType) + } +} + +func testApplyChanges(t *testing.T, changeType ChangeType) { + t.Helper() + ctx := context.Background() + validZoneResponse := getValidResponseZoneALlBytes(t) + validRRSetResponse := getValidResponseRRSetAllBytes(t) + invalidZoneResponse := []byte(`{"invalid: "json"`) + + // Test cases + tests := getApplyChangesBasicTestCases(validZoneResponse, validRRSetResponse, invalidZoneResponse) + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + // Set up common endpoint for all types of changes + setUpCommonEndpoints(mux, tt.responseZone, tt.responseZoneCode) + + // Set up change type-specific endpoints + setUpChangeTypeEndpoints(t, mux, tt.responseRrset, tt.responseRrsetCode, changeType) + + defer server.Close() + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + // Set up the changes according to the change type + changes := getChangeTypeChanges(changeType) + + err = dnsProvider.ApplyChanges(ctx, changes) + if tt.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestNoMatchingZoneFound(t *testing.T) { + t.Parallel() + + ctx := context.Background() + validZoneResponse := getValidResponseZoneALlBytes(t) + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + defer server.Close() + + // Set up common endpoint for all types of changes + setUpCommonEndpoints(mux, validZoneResponse, http.StatusOK) + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + {DNSName: "notfound.com", Targets: endpoint.Targets{"test.notfound.com"}}, + }, + UpdateNew: []*endpoint.Endpoint{}, + Delete: []*endpoint.Endpoint{}, + } + + err = dnsProvider.ApplyChanges(ctx, changes) + assert.Error(t, err) +} + +func TestNoRRSetFound(t *testing.T) { + t.Parallel() + + ctx := context.Background() + validZoneResponse := getValidResponseZoneALlBytes(t) + rrSets := getValidResponseRRSetAll() + rrSets.GetItems()[0].Name = "notfound.test.com" + validRRSetResponse, err := json.Marshal(rrSets) + assert.NoError(t, err) + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + defer server.Close() + + // Set up common endpoint for all types of changes + setUpCommonEndpoints(mux, validZoneResponse, http.StatusOK) + + mux.HandleFunc( + "/zones/1234/rrset", + responseHandler(validRRSetResponse, http.StatusOK), + ) + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + changes := &plan.Changes{ + UpdateNew: []*endpoint.Endpoint{ + {DNSName: "test.com", Targets: endpoint.Targets{"notfound.test.com"}}, + }, + } + + err = dnsProvider.ApplyChanges(ctx, changes) + assert.Error(t, err) +} + +// setUpCommonEndpoints for all change types. +func setUpCommonEndpoints(mux *http.ServeMux, responseZone []byte, responseZoneCode int) { + mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(responseZoneCode) + w.Write(responseZone) + }) +} + +// setUpChangeTypeEndpoints for type-specific endpoints. +func setUpChangeTypeEndpoints( + t *testing.T, + mux *http.ServeMux, + responseRrset []byte, + responseRrsetCode int, + changeType ChangeType, +) { + t.Helper() + + switch changeType { + case Create: + mux.HandleFunc( + "/zones/1234/rrset", + responseHandler(responseRrset, responseRrsetCode), + ) + case Update: + mux.HandleFunc( + "/zones/1234/rrset/1234", + responseHandler(responseRrset, responseRrsetCode), + ) + mux.HandleFunc( + "/zones/1234/rrset", + func(w http.ResponseWriter, r *http.Request) { + getRrsetsResponseRecords(t, w, "1234") + }, + ) + mux.HandleFunc( + "/zones/5678/rrset", + func(w http.ResponseWriter, r *http.Request) { + getRrsetsResponseRecords(t, w, "5678") + }, + ) + case Delete: + responseCode := responseRrsetCode + if responseCode == http.StatusOK { + responseCode = http.StatusNoContent + } + mux.HandleFunc( + "/zones/1234/rrset/1234", + responseHandler(nil, responseCode), + ) + mux.HandleFunc( + "/zones/1234/rrset", + func(w http.ResponseWriter, r *http.Request) { + getRrsetsResponseRecords(t, w, "1234") + }, + ) + mux.HandleFunc( + "/zones/5678/rrset", + func(w http.ResponseWriter, r *http.Request) { + getRrsetsResponseRecords(t, w, "5678") + }, + ) + } +} + +// getChangeTypeChanges according to the change type. +func getChangeTypeChanges(changeType ChangeType) *plan.Changes { + switch changeType { + case Create: + return &plan.Changes{ + Create: []*endpoint.Endpoint{ + {DNSName: "test.com", Targets: endpoint.Targets{"test.test.com"}}, + }, + UpdateNew: []*endpoint.Endpoint{}, + Delete: []*endpoint.Endpoint{}, + } + case Update: + return &plan.Changes{ + UpdateNew: []*endpoint.Endpoint{ + {DNSName: "test.com", Targets: endpoint.Targets{"test.com"}, RecordType: "A"}, + }, + } + case Delete: + return &plan.Changes{ + Delete: []*endpoint.Endpoint{ + {DNSName: "test.com", Targets: endpoint.Targets{"test.com"}, RecordType: "A"}, + }, + } + default: + return nil + } +} + +func getApplyChangesBasicTestCases( //nolint:funlen // Test cases are long + validZoneResponse []byte, + validRRSetResponse []byte, + invalidZoneResponse []byte, +) []struct { + name string + responseZone []byte + responseZoneCode int + responseRrset []byte + responseRrsetCode int + expectErr bool + expectedRrsetMethod string +} { + tests := []struct { + name string + responseZone []byte + responseZoneCode int + responseRrset []byte + responseRrsetCode int + expectErr bool + expectedRrsetMethod string + }{ + { + "Valid response", + validZoneResponse, + http.StatusOK, + validRRSetResponse, + http.StatusOK, + false, + http.MethodPost, + }, + { + "Zone response 403", + nil, + http.StatusForbidden, + validRRSetResponse, + http.StatusAccepted, + true, + "", + }, + { + "Zone response 500", + nil, + http.StatusInternalServerError, + validRRSetResponse, + http.StatusAccepted, + true, + "", + }, + { + "Zone response Invalid JSON", + invalidZoneResponse, + http.StatusOK, + validRRSetResponse, + http.StatusAccepted, + true, + "", + }, + { + "Zone response, Rrset response 403", + validZoneResponse, + http.StatusOK, + nil, + http.StatusForbidden, + true, + http.MethodPost, + }, + { + "Zone response, Rrset response 500", + validZoneResponse, + http.StatusOK, + nil, + http.StatusInternalServerError, + true, + http.MethodPost, + }, + } + + return tests +} + +func responseHandler(responseBody []byte, statusCode int) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + if responseBody != nil { + w.Write(responseBody) + } + } +} + +func getValidResponseZoneALlBytes(t *testing.T) []byte { + t.Helper() + + zones := getValidZoneResponseAll() + validZoneResponse, err := json.Marshal(zones) + assert.NoError(t, err) + + return validZoneResponse +} + +func getValidZoneResponseAll() domains.List[domains.Zone] { + return domains.List[domains.Zone]{ + Count: 2, + NextOffset: 0, + Items: []*domains.Zone{ + {ID: "1234", Name: "test.com"}, + {ID: "5678", Name: "test2.com"}, + }, + } +} + +func getValidResponseRRSetAllBytes(t *testing.T) []byte { + t.Helper() + + rrSets := getValidResponseRRSetAll() + validRRSetResponse, err := json.Marshal(rrSets) + assert.NoError(t, err) + + return validRRSetResponse +} + +func getValidResponseRRSetAll() domains.List[domains.RRSet] { + return domains.List[domains.RRSet]{ + Count: 1, + NextOffset: 0, + Items: []*domains.RRSet{ + { + ID: "1234", + Name: "test.com.", + Type: "A", + TTL: 300, + Records: []domains.RecordItem{ + {Content: "1.2.3.4"}, + }, + }, + }, + } +} diff --git a/internal/selprovider/config.go b/internal/selprovider/config.go new file mode 100644 index 0000000..9c668a9 --- /dev/null +++ b/internal/selprovider/config.go @@ -0,0 +1,19 @@ +package selprovider + +import ( + "sigs.k8s.io/external-dns/endpoint" +) + +// Config is used to configure the creation of the Provider. +type Config struct { + BaseURL string + KeystoneProvider KeystoneProvider + DomainFilter endpoint.DomainFilter + DryRun bool + Workers int +} + +//go:generate mockgen -destination=./mock/keystone_provider.go -source=./config.go KeystoneProvider +type KeystoneProvider interface { + GetToken() (string, error) +} diff --git a/internal/selprovider/domain_filter.go b/internal/selprovider/domain_filter.go new file mode 100644 index 0000000..a1e73f1 --- /dev/null +++ b/internal/selprovider/domain_filter.go @@ -0,0 +1,7 @@ +package selprovider + +import "sigs.k8s.io/external-dns/endpoint" + +func (p *Provider) GetDomainFilter() endpoint.DomainFilter { + return p.domainFilter +} diff --git a/internal/selprovider/domain_filter_test.go b/internal/selprovider/domain_filter_test.go new file mode 100644 index 0000000..a154356 --- /dev/null +++ b/internal/selprovider/domain_filter_test.go @@ -0,0 +1,21 @@ +package selprovider + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "sigs.k8s.io/external-dns/endpoint" +) + +func TestGetDomainFilter(t *testing.T) { + t.Parallel() + + server := getServerRecords(t) + defer server.Close() + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 0)) + assert.NoError(t, err) + + domainFilter := dnsProvider.GetDomainFilter() + assert.Equal(t, domainFilter, endpoint.DomainFilter{}) +} diff --git a/internal/selprovider/helper.go b/internal/selprovider/helper.go new file mode 100644 index 0000000..4ac5108 --- /dev/null +++ b/internal/selprovider/helper.go @@ -0,0 +1,86 @@ +package selprovider + +import ( + "strings" + + domains "github.com/selectel/domains-go/pkg/v2" + "go.uber.org/zap" + "sigs.k8s.io/external-dns/endpoint" +) + +// findBestMatchingZone finds the best matching zone for a given record set name. The criteria are +// that the zone name is contained in the record set name and that the zone name is the longest +// possible match. Eg foo.bar.com. would have prejudice over bar.com. if rr set name is foo.bar.com. +func findBestMatchingZone(rrSetName string, zones []*domains.Zone) (*domains.Zone, bool) { + count := 0 + var domainZone *domains.Zone + for _, zone := range zones { + if len(zone.Name) > count && strings.Contains(appendDotIfNotExists(rrSetName), zone.Name) { + count = len(zone.Name) + domainZone = zone + } + } + + if count == 0 { + return nil, false + } + + return domainZone, true +} + +// findRRSet finds a record set by name and type in a list of record sets. +func findRRSet(rrSetName, recordType string, rrSets []*domains.RRSet) (*domains.RRSet, bool) { + for _, rrSet := range rrSets { + if rrSet.Name == rrSetName && string(rrSet.Type) == recordType { + return rrSet, true + } + } + + return nil, false +} + +// appendDotIfNotExists appends a dot to the end of a string if it doesn't already end with a dot. +func appendDotIfNotExists(s string) string { + if !strings.HasSuffix(s, ".") { + return s + "." + } + + return s +} + +// modifyChange modifies a change to ensure it is valid for this provider. +func modifyChange(change *endpoint.Endpoint) { + change.DNSName = appendDotIfNotExists(change.DNSName) + + if change.RecordTTL == 0 { + change.RecordTTL = 300 + } +} + +// getRRSetRecord returns a v2.RRSet from a change for the api client. +func getRRSetRecord(change *endpoint.Endpoint) *domains.RRSet { + records := make([]domains.RecordItem, len(change.Targets)) + for i, target := range change.Targets { + records[i] = domains.RecordItem{ + Content: target, + } + } + + return &domains.RRSet{ + Name: change.DNSName, + Records: records, + TTL: int(change.RecordTTL), + Type: domains.RecordType(change.RecordType), + } +} + +// getLogFields returns a log.Fields object for a change. +func getLogFields(change *endpoint.Endpoint, action string, id string) []zap.Field { + return []zap.Field{ + zap.String("record", change.DNSName), + zap.String("content", strings.Join(change.Targets, ",")), + zap.String("type", change.RecordType), + zap.String("action", action), + zap.String("id", id), + } +} diff --git a/internal/selprovider/helper_test.go b/internal/selprovider/helper_test.go new file mode 100644 index 0000000..c484268 --- /dev/null +++ b/internal/selprovider/helper_test.go @@ -0,0 +1,183 @@ +package selprovider + +import ( + "reflect" + "testing" + + domains "github.com/selectel/domains-go/pkg/v2" + "go.uber.org/zap" + "sigs.k8s.io/external-dns/endpoint" +) + +func TestAppendDotIfNotExists(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + s string + want string + }{ + {"No dot at end", "test", "test."}, + {"Dot at end", "test.", "test."}, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + if got := appendDotIfNotExists(tt.s); got != tt.want { + t.Errorf("appendDotIfNotExists() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestModifyChange(t *testing.T) { + t.Parallel() + + endpointWithTTL := &endpoint.Endpoint{ + DNSName: "test", + RecordTTL: endpoint.TTL(400), + } + modifyChange(endpointWithTTL) + if endpointWithTTL.DNSName != "test." { + t.Errorf("modifyChange() did not append dot to DNSName = %v, want test.", endpointWithTTL.DNSName) + } + if endpointWithTTL.RecordTTL != 400 { + t.Errorf("modifyChange() changed existing RecordTTL = %v, want 400", endpointWithTTL.RecordTTL) + } + + endpointWithoutTTL := &endpoint.Endpoint{ + DNSName: "test", + } + modifyChange(endpointWithoutTTL) + if endpointWithoutTTL.DNSName != "test." { + t.Errorf("modifyChange() did not append dot to DNSName = %v, want test.", endpointWithoutTTL.DNSName) + } + if endpointWithoutTTL.RecordTTL != 300 { + t.Errorf("modifyChange() did not set default RecordTTL = %v, want 300", endpointWithoutTTL.RecordTTL) + } +} + +func TestGetRRSetRecordPost(t *testing.T) { + t.Parallel() + + change := &endpoint.Endpoint{ + DNSName: "test.", + RecordTTL: endpoint.TTL(300), + RecordType: "A", + Targets: endpoint.Targets{ + "192.0.2.1", + "192.0.2.2", + }, + } + expected := &domains.RRSet{ + Name: "test.", + TTL: 300, + Type: "A", + Records: []domains.RecordItem{ + { + Content: "192.0.2.1", + }, + { + Content: "192.0.2.2", + }, + }, + } + got := getRRSetRecord(change) + if !reflect.DeepEqual(got, expected) { + t.Errorf("getRRSetRecord() = %v, want %v", got, expected) + } +} + +func TestFindBestMatchingZone(t *testing.T) { + t.Parallel() + + zones := []*domains.Zone{ + {Name: "foo.com"}, + {Name: "bar.com"}, + {Name: "baz.com"}, + } + + tests := []struct { + name string + rrSetName string + want *domains.Zone + wantFound bool + }{ + {"Matching Zone", "www.foo.com", zones[0], true}, + {"No Matching Zone", "www.test.com", nil, false}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, found := findBestMatchingZone(tt.rrSetName, zones) + if !reflect.DeepEqual(got, tt.want) || found != tt.wantFound { + t.Errorf("findBestMatchingZone() = %v, %v, want %v, %v", got, found, tt.want, tt.wantFound) + } + }) + } +} + +func TestFindRRSet(t *testing.T) { + t.Parallel() + + rrSets := []*domains.RRSet{ + {Name: "www.foo.com", Type: "A"}, + {Name: "www.bar.com", Type: "A"}, + {Name: "www.baz.com", Type: "A"}, + } + + tests := []struct { + name string + rrSetName string + recordType string + want *domains.RRSet + wantFound bool + }{ + {"Matching RRSet", "www.foo.com", "A", rrSets[0], true}, + {"No Matching RRSet", "www.test.com", "A", nil, false}, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, found := findRRSet(tt.rrSetName, tt.recordType, rrSets) + if !reflect.DeepEqual(got, tt.want) || found != tt.wantFound { + t.Errorf("findRRSet() = %v, %v, want %v, %v", got, found, tt.want, tt.wantFound) + } + }) + } +} + +func TestGetLogFields(t *testing.T) { + t.Parallel() + + change := &endpoint.Endpoint{ + DNSName: "test.", + RecordTTL: endpoint.TTL(300), + RecordType: "A", + Targets: endpoint.Targets{ + "192.0.2.1", + "192.0.2.2", + }, + } + + expected := []zap.Field{ + zap.String("record", "test."), + zap.String("content", "192.0.2.1,192.0.2.2"), + zap.String("type", "A"), + zap.String("action", "create"), + zap.String("id", "123"), + } + + got := getLogFields(change, "create", "123") + + if !reflect.DeepEqual(got, expected) { + t.Errorf("getLogFields() = %v, want %v", got, expected) + } +} diff --git a/internal/selprovider/mock/keystone_provider.go b/internal/selprovider/mock/keystone_provider.go new file mode 100644 index 0000000..aa724d0 --- /dev/null +++ b/internal/selprovider/mock/keystone_provider.go @@ -0,0 +1,54 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./config.go +// +// Generated by this command: +// +// mockgen -destination=./mock/keystone_provider.go -source=./config.go KeystoneProvider +// + +// Package mock_selprovider is a generated GoMock package. +package mock_selprovider + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" +) + +// MockKeystoneProvider is a mock of KeystoneProvider interface. +type MockKeystoneProvider struct { + ctrl *gomock.Controller + recorder *MockKeystoneProviderMockRecorder +} + +// MockKeystoneProviderMockRecorder is the mock recorder for MockKeystoneProvider. +type MockKeystoneProviderMockRecorder struct { + mock *MockKeystoneProvider +} + +// NewMockKeystoneProvider creates a new mock instance. +func NewMockKeystoneProvider(ctrl *gomock.Controller) *MockKeystoneProvider { + mock := &MockKeystoneProvider{ctrl: ctrl} + mock.recorder = &MockKeystoneProviderMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKeystoneProvider) EXPECT() *MockKeystoneProviderMockRecorder { + return m.recorder +} + +// GetToken mocks base method. +func (m *MockKeystoneProvider) GetToken() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetToken") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetToken indicates an expected call of GetToken. +func (mr *MockKeystoneProviderMockRecorder) GetToken() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToken", reflect.TypeOf((*MockKeystoneProvider)(nil).GetToken)) +} diff --git a/internal/selprovider/models.go b/internal/selprovider/models.go new file mode 100644 index 0000000..645da47 --- /dev/null +++ b/internal/selprovider/models.go @@ -0,0 +1,21 @@ +package selprovider + +import "sigs.k8s.io/external-dns/endpoint" + +const ( + CREATE = "CREATE" + UPDATE = "UPDATE" + DELETE = "DELETE" +) + +// changeTask is a task that is passed to the worker. +type changeTask struct { + change *endpoint.Endpoint + action string +} + +// endpointError is a list of endpoints and an error to pass to workers. +type endpointError struct { + endpoints []*endpoint.Endpoint + err error +} diff --git a/internal/selprovider/provider.go b/internal/selprovider/provider.go new file mode 100644 index 0000000..ef49a18 --- /dev/null +++ b/internal/selprovider/provider.go @@ -0,0 +1,53 @@ +package selprovider + +import ( + "net/http" + + domains "github.com/selectel/domains-go/pkg/v2" + "github.com/selectel/external-dns-webhook/pkg/httpclient" + "go.uber.org/zap" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/provider" +) + +// Provider implements the DNS provider interface for Selectel DNS. +type Provider struct { + provider.BaseProvider + domainFilter endpoint.DomainFilter + keystoneProvider KeystoneProvider + endpoint string + dryRun bool + workers int + logger *zap.Logger + zoneFetcherClient *zoneFetcher + rrSetFetcherClient *rrSetFetcher +} + +// getDomainsClient returns v2.DNSClient with provided keystone and user-agent from httpclient.DefaultUserAgent +func (p *Provider) getDomainsClient() (domains.DNSClient[domains.Zone, domains.RRSet], error) { + token, err := p.keystoneProvider.GetToken() + if err != nil { + p.logger.Error("authorization error during getting keystone token", zap.Error(err)) + return nil, err + } + + httpClient := httpclient.Default() + headers := http.Header{} + headers.Add("X-Auth-Token", token) + headers.Add("User-Agent", httpclient.DefaultUserAgent) + return domains.NewClient(p.endpoint, &httpClient, headers), nil +} + +// New creates a new Selectel DNS provider. +func New(config Config, logger *zap.Logger) (*Provider, error) { + return &Provider{ + domainFilter: config.DomainFilter, + dryRun: config.DryRun, + workers: config.Workers, + logger: logger, + keystoneProvider: config.KeystoneProvider, + endpoint: config.BaseURL, + zoneFetcherClient: newZoneFetcher(config.DomainFilter), + rrSetFetcherClient: newRRSetFetcher(config.DomainFilter, logger), + }, nil +} diff --git a/internal/selprovider/records.go b/internal/selprovider/records.go new file mode 100644 index 0000000..c10883a --- /dev/null +++ b/internal/selprovider/records.go @@ -0,0 +1,111 @@ +package selprovider + +import ( + "context" + + domains "github.com/selectel/domains-go/pkg/v2" + "sigs.k8s.io/external-dns/endpoint" + "sigs.k8s.io/external-dns/provider" +) + +// Records returns resource records. +func (p *Provider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + client, err := p.getDomainsClient() + if err != nil { + return nil, err + } + + zones, err := p.zoneFetcherClient.zones(ctx, client) + if err != nil { + return nil, err + } + + var endpoints []*endpoint.Endpoint + endpointsErrorChannel := make(chan endpointError, len(zones)) + zonesChan := make(chan string, len(zones)) + + for i := 0; i < p.workers; i++ { + go p.fetchRecordsWorker(ctx, client, zonesChan, endpointsErrorChannel) + } + + for _, zone := range zones { + zonesChan <- zone.ID + } + + for i := 0; i < len(zones); i++ { + endpointsErrorList := <-endpointsErrorChannel + if endpointsErrorList.err != nil { + close(zonesChan) + + return nil, endpointsErrorList.err + } + endpoints = append(endpoints, endpointsErrorList.endpoints...) + } + + close(zonesChan) + + return endpoints, nil +} + +// fetchRecordsWorker fetches all records from a given zone. +func (p *Provider) fetchRecordsWorker( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + zonesChan chan string, + endpointsErrorChan chan<- endpointError, +) { + for zoneID := range zonesChan { + p.processZoneRRSets(ctx, client, zoneID, endpointsErrorChan) + } + + p.logger.Debug("fetch record set worker finished") +} + +// processZoneRRSets fetches and processes DNS records for a given zone. +func (p *Provider) processZoneRRSets( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + zoneID string, + endpointsErrorChannel chan<- endpointError, +) { + var endpoints []*endpoint.Endpoint + rrSets, err := p.rrSetFetcherClient.fetchRecords(ctx, client, zoneID, map[string]string{}) + if err != nil { + endpointsErrorChannel <- endpointError{ + endpoints: nil, + err: err, + } + + return + } + + endpoints = p.collectEndPoints(rrSets) + endpointsErrorChannel <- endpointError{ + endpoints: endpoints, + err: nil, + } +} + +// collectEndPoints creates a list of Endpoints from the provided rrSets. +func (p *Provider) collectEndPoints( + rrSets []*domains.RRSet, +) []*endpoint.Endpoint { + var endpoints []*endpoint.Endpoint + for _, rrSet := range rrSets { + if provider.SupportedRecordType(string(rrSet.Type)) { + for _, rec := range rrSet.Records { + endpoints = append( + endpoints, + endpoint.NewEndpointWithTTL( + rrSet.Name, + string(rrSet.Type), + endpoint.TTL(rrSet.TTL), + rec.Content, + ), + ) + } + } + } + + return endpoints +} diff --git a/internal/selprovider/records_test.go b/internal/selprovider/records_test.go new file mode 100644 index 0000000..7b8a6c4 --- /dev/null +++ b/internal/selprovider/records_test.go @@ -0,0 +1,268 @@ +package selprovider + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/goccy/go-json" + domains "github.com/selectel/domains-go/pkg/v2" + mock_selprovider "github.com/selectel/external-dns-webhook/internal/selprovider/mock" + "github.com/stretchr/testify/assert" + "go.uber.org/mock/gomock" + "go.uber.org/zap" + "sigs.k8s.io/external-dns/endpoint" +) + +func TestRecords(t *testing.T) { + t.Parallel() + + server := getServerRecords(t) + defer server.Close() + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + endpoints, err := dnsProvider.Records(context.Background()) + assert.NoError(t, err) + assert.Equal(t, 2, len(endpoints)) + assert.Equal(t, "test.com", endpoints[0].DNSName) + assert.Equal(t, "A", endpoints[0].RecordType) + assert.Equal(t, "1.2.3.4", endpoints[0].Targets[0]) + assert.Equal(t, int64(300), int64(endpoints[0].RecordTTL)) + + assert.Equal(t, "test2.com", endpoints[1].DNSName) + assert.Equal(t, "A", endpoints[1].RecordType) + assert.Equal(t, "5.6.7.8", endpoints[1].Targets[0]) + assert.Equal(t, int64(300), int64(endpoints[1].RecordTTL)) +} + +// TestWrongJsonResponseRecords tests the scenario where the server returns a wrong JSON response. +func TestWrongJsonResponseRecords(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + mux.HandleFunc("/zones", + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"invalid:"json"`)) // This is not a valid JSON. + }, + ) + defer server.Close() + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + _, err = dnsProvider.Records(context.Background()) + assert.Error(t, err) +} + +func TestEmptyZonesRouteRecords(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + defer server.Close() + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + _, err = dnsProvider.Records(context.Background()) + assert.Error(t, err) +} + +func TestEmptyRRSetRouteRecords(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + mux.HandleFunc("/zones", + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + zones := domains.List[domains.Zone]{ + Count: 1, + NextOffset: 0, + Items: []*domains.Zone{{ + ID: "1234", + }}, + } + successResponseBytes, err := json.Marshal(zones) + assert.NoError(t, err) + + w.WriteHeader(http.StatusOK) + w.Write(successResponseBytes) + }, + ) + defer server.Close() + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + _, err = dnsProvider.Records(context.Background()) + fmt.Println(err) + assert.Error(t, err) +} + +func TestZoneEndpoint500Records(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + mux.HandleFunc("/zones", + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusInternalServerError) + }, + ) + defer server.Close() + + dnsProvider, err := getDefaultTestProvider(server, getDefaultKeystoneProvider(t, 1)) + assert.NoError(t, err) + + _, err = dnsProvider.Records(context.Background()) + assert.Error(t, err) +} + +func TestZoneEndpoint403Records(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + mux.HandleFunc("/zones", + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + w.WriteHeader(http.StatusForbidden) + }, + ) + defer server.Close() + + dnsProvider, err := New(Config{ + BaseURL: server.URL, + DomainFilter: endpoint.DomainFilter{}, + KeystoneProvider: getDefaultKeystoneProvider(t, 1), + DryRun: false, + Workers: 10, + }, zap.NewNop()) + assert.NoError(t, err) + + _, err = dnsProvider.Records(context.Background()) + assert.Error(t, err) +} + +func getDefaultKeystoneProvider(t *testing.T, callTimes int) KeystoneProvider { + ctrl := gomock.NewController(t) + t.Cleanup(ctrl.Finish) + + p := mock_selprovider.NewMockKeystoneProvider(ctrl) + p.EXPECT().GetToken().Return("test", nil).Times(callTimes) + + return p +} + +func getDefaultTestProvider(server *httptest.Server, keystoneProvider KeystoneProvider) (*Provider, error) { + dnsProvider, err := New(Config{ + BaseURL: server.URL, + KeystoneProvider: keystoneProvider, + DomainFilter: endpoint.DomainFilter{}, + DryRun: false, + Workers: 1, + }, zap.NewNop()) + + return dnsProvider, err +} + +func getZonesResponseRecords(t *testing.T, w http.ResponseWriter) { + t.Helper() + + w.Header().Set("Content-Type", "application/json") + + zones := domains.List[domains.Zone]{ + Count: 2, + NextOffset: 0, + Items: []*domains.Zone{ + {ID: "1234", Name: "test.com"}, + {ID: "5678", Name: "test2.com"}, + }, + } + successResponseBytes, err := json.Marshal(zones) + assert.NoError(t, err) + + w.WriteHeader(http.StatusOK) + w.Write(successResponseBytes) +} + +func getRrsetsResponseRecords(t *testing.T, w http.ResponseWriter, domain string) { + t.Helper() + + w.Header().Set("Content-Type", "application/json") + + rrSets := domains.List[domains.RRSet]{} + if domain == "1234" { + rrSets = domains.List[domains.RRSet]{ + Count: 1, + NextOffset: 0, + Items: []*domains.RRSet{ + { + ID: "1234", + Name: "test.com.", + Type: "A", + TTL: 300, + Records: []domains.RecordItem{ + {Content: "1.2.3.4"}, + }, + }, + }, + } + } + if domain == "5678" { + rrSets = domains.List[domains.RRSet]{ + Count: 1, + NextOffset: 0, + Items: []*domains.RRSet{ + { + ID: "5678", + Name: "test2.com.", + Type: "A", + TTL: 300, + Records: []domains.RecordItem{ + {Content: "5.6.7.8"}, + }, + }, + }, + } + } + + successResponseBytes, err := json.Marshal(rrSets) + assert.NoError(t, err) + + w.WriteHeader(http.StatusOK) + w.Write(successResponseBytes) +} + +func getServerRecords(t *testing.T) *httptest.Server { + t.Helper() + + mux := http.NewServeMux() + server := httptest.NewServer(mux) + + mux.HandleFunc("/zones", func(w http.ResponseWriter, r *http.Request) { + getZonesResponseRecords(t, w) + }) + mux.HandleFunc("/zones/1234/rrset", func(w http.ResponseWriter, r *http.Request) { + getRrsetsResponseRecords(t, w, "1234") + }) + mux.HandleFunc("/zones/5678/rrset", func(w http.ResponseWriter, r *http.Request) { + getRrsetsResponseRecords(t, w, "5678") + }) + + return server +} diff --git a/internal/selprovider/rrset_fetcher.go b/internal/selprovider/rrset_fetcher.go new file mode 100644 index 0000000..cb15f0a --- /dev/null +++ b/internal/selprovider/rrset_fetcher.go @@ -0,0 +1,90 @@ +package selprovider + +import ( + "context" + "fmt" + "strconv" + + domains "github.com/selectel/domains-go/pkg/v2" + "go.uber.org/zap" + "sigs.k8s.io/external-dns/endpoint" +) + +type rrSetFetcher struct { + domainFilter endpoint.DomainFilter + projectId string + logger *zap.Logger +} + +func newRRSetFetcher( + domainFilter endpoint.DomainFilter, + logger *zap.Logger, +) *rrSetFetcher { + return &rrSetFetcher{ + domainFilter: domainFilter, + logger: logger, + } +} + +// fetchRecords fetches all []v2.RRSet from Selectel DNS API for given zone id in options["filter"]. +func (r *rrSetFetcher) fetchRecords( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + zoneId string, + options map[string]string, +) ([]*domains.RRSet, error) { + options["limit"] = "1000" + options["offset"] = "0" + + var rrSets []*domains.RRSet + + for { + rrSetsResponse, err := client.ListRRSets(ctx, zoneId, &options) + if err != nil { + return nil, err + } + + rrSets = append(rrSets, rrSetsResponse.GetItems()...) + + options["offset"] = strconv.Itoa(rrSetsResponse.GetNextOffset()) + if rrSetsResponse.GetNextOffset() == 0 { + break + } + } + + return rrSets, nil +} + +// getRRSetForUpdateDeletion returns the record set to be deleted and the zone it belongs to. +func (r *rrSetFetcher) getRRSetForUpdateDeletion( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + change *endpoint.Endpoint, + zones []*domains.Zone, +) (*domains.Zone, *domains.RRSet, error) { + resultZone, found := findBestMatchingZone(change.DNSName, zones) + if !found { + r.logger.Error( + "record set name contains no zone dns name", + zap.String("name", change.DNSName), + ) + + return nil, nil, fmt.Errorf("record set name contains no zone dns name") + } + + domainRrSets, err := r.fetchRecords(ctx, client, resultZone.ID, map[string]string{ + "name": change.DNSName, + }) + if err != nil { + return nil, nil, err + } + + resultRRSet, found := findRRSet(change.DNSName, change.RecordType, domainRrSets) + if !found { + r.logger.Info("record not found on record sets", zap.String("name", change.DNSName)) + + return nil, nil, fmt.Errorf("record not found on record sets") + } + + return resultZone, resultRRSet, nil +} diff --git a/internal/selprovider/zone_fetcher.go b/internal/selprovider/zone_fetcher.go new file mode 100644 index 0000000..29662b6 --- /dev/null +++ b/internal/selprovider/zone_fetcher.go @@ -0,0 +1,75 @@ +package selprovider + +import ( + "context" + "strconv" + + domains "github.com/selectel/domains-go/pkg/v2" + "sigs.k8s.io/external-dns/endpoint" +) + +type zoneFetcher struct { + domainFilter endpoint.DomainFilter +} + +func newZoneFetcher( + domainFilter endpoint.DomainFilter, +) *zoneFetcher { + return &zoneFetcher{ + domainFilter: domainFilter, + } +} + +// zones returns filtered list of v2.Zone if domainFilter is set. +func (z *zoneFetcher) zones(ctx context.Context, client domains.DNSClient[domains.Zone, domains.RRSet]) ([]*domains.Zone, error) { + if len(z.domainFilter.Filters) == 0 { + zones, err := z.fetchZones(ctx, client, map[string]string{}) + if err != nil { + return nil, err + } + + return zones, nil + } + + var result []*domains.Zone + // send one request per filter + for _, filter := range z.domainFilter.Filters { + zones, err := z.fetchZones(ctx, client, map[string]string{ + "filter": filter, + }) + if err != nil { + return nil, err + } + result = append(result, zones...) + } + + return result, nil +} + +// fetchZones fetches all []v2.Zone from Selectel DNS API. It may be filtered with options["filter"] provided. +func (z *zoneFetcher) fetchZones( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + options map[string]string, +) ([]*domains.Zone, error) { + options["limit"] = "1000" + options["offset"] = "0" + + var zones []*domains.Zone + + for { + zonesResponse, err := client.ListZones(ctx, &options) + if err != nil { + return nil, err + } + + zones = append(zones, zonesResponse.GetItems()...) + + options["offset"] = strconv.Itoa(zonesResponse.GetNextOffset()) + if zonesResponse.GetNextOffset() == 0 { + break + } + } + + return zones, nil +} diff --git a/pkg/api/adjust_endpoints_test.go b/pkg/api/adjust_endpoints_test.go index 2694cd4..78cd26e 100644 --- a/pkg/api/adjust_endpoints_test.go +++ b/pkg/api/adjust_endpoints_test.go @@ -8,8 +8,8 @@ import ( "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/api.go b/pkg/api/api.go index 314dab6..2424570 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -9,13 +9,13 @@ import ( "syscall" "time" - json "github.com/goccy/go-json" + "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/helmet" fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/pprof" fiberrecover "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/metrics" + "github.com/selectel/external-dns-webhook/pkg/metrics" "go.uber.org/zap" "sigs.k8s.io/external-dns/provider" ) diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index 9a1b5c4..c2c4968 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/apply_changes_test.go b/pkg/api/apply_changes_test.go index 256acd1..fc4208b 100644 --- a/pkg/api/apply_changes_test.go +++ b/pkg/api/apply_changes_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/domain_filter_test.go b/pkg/api/domain_filter_test.go index b117f3d..0985ea2 100644 --- a/pkg/api/domain_filter_test.go +++ b/pkg/api/domain_filter_test.go @@ -8,8 +8,8 @@ import ( "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/health_test.go b/pkg/api/health_test.go index b1b00be..6062081 100644 --- a/pkg/api/health_test.go +++ b/pkg/api/health_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index 342a509..bf12957 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -7,7 +7,7 @@ import ( "github.com/gofiber/adaptor/v2" "github.com/gofiber/fiber/v2" "github.com/prometheus/client_golang/prometheus/promhttp" - metrics_collector "github.com/stackitcloud/external-dns-stackit-webhook/pkg/metrics" + metrics_collector "github.com/selectel/external-dns-webhook/pkg/metrics" ) // registerAt registers the metrics endpoint. diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 0b49546..994bbff 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - metrics_collector "github.com/stackitcloud/external-dns-stackit-webhook/pkg/metrics" - mock_metrics_collector "github.com/stackitcloud/external-dns-stackit-webhook/pkg/metrics/mock" + "github.com/selectel/external-dns-webhook/pkg/api" + metrics_collector "github.com/selectel/external-dns-webhook/pkg/metrics" + mock_metrics_collector "github.com/selectel/external-dns-webhook/pkg/metrics/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) diff --git a/pkg/api/mock/api.go b/pkg/api/mock/api.go index 435702d..e2a0f8f 100644 --- a/pkg/api/mock/api.go +++ b/pkg/api/mock/api.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: ./api.go +// +// Generated by this command: +// +// mockgen -destination=./mock/api.go -source=./api.go Provider +// // Package mock_api is a generated GoMock package. package mock_api @@ -46,7 +51,7 @@ func (m *MockApi) Listen(port string) error { } // Listen indicates an expected call of Listen. -func (mr *MockApiMockRecorder) Listen(port interface{}) *gomock.Call { +func (mr *MockApiMockRecorder) Listen(port any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Listen", reflect.TypeOf((*MockApi)(nil).Listen), port) } @@ -54,7 +59,7 @@ func (mr *MockApiMockRecorder) Listen(port interface{}) *gomock.Call { // Test mocks base method. func (m *MockApi) Test(req *http.Request, msTimeout ...int) (*http.Response, error) { m.ctrl.T.Helper() - varargs := []interface{}{req} + varargs := []any{req} for _, a := range msTimeout { varargs = append(varargs, a) } @@ -65,9 +70,9 @@ func (m *MockApi) Test(req *http.Request, msTimeout ...int) (*http.Response, err } // Test indicates an expected call of Test. -func (mr *MockApiMockRecorder) Test(req interface{}, msTimeout ...interface{}) *gomock.Call { +func (mr *MockApiMockRecorder) Test(req any, msTimeout ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{req}, msTimeout...) + varargs := append([]any{req}, msTimeout...) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Test", reflect.TypeOf((*MockApi)(nil).Test), varargs...) } @@ -103,7 +108,7 @@ func (m *MockProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoi } // AdjustEndpoints indicates an expected call of AdjustEndpoints. -func (mr *MockProviderMockRecorder) AdjustEndpoints(endpoints interface{}) *gomock.Call { +func (mr *MockProviderMockRecorder) AdjustEndpoints(endpoints any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AdjustEndpoints", reflect.TypeOf((*MockProvider)(nil).AdjustEndpoints), endpoints) } @@ -117,7 +122,7 @@ func (m *MockProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) } // ApplyChanges indicates an expected call of ApplyChanges. -func (mr *MockProviderMockRecorder) ApplyChanges(ctx, changes interface{}) *gomock.Call { +func (mr *MockProviderMockRecorder) ApplyChanges(ctx, changes any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ApplyChanges", reflect.TypeOf((*MockProvider)(nil).ApplyChanges), ctx, changes) } @@ -146,7 +151,7 @@ func (m *MockProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error } // Records indicates an expected call of Records. -func (mr *MockProviderMockRecorder) Records(ctx interface{}) *gomock.Call { +func (mr *MockProviderMockRecorder) Records(ctx any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Records", reflect.TypeOf((*MockProvider)(nil).Records), ctx) } diff --git a/pkg/api/records_test.go b/pkg/api/records_test.go index b5486fe..a57c94c 100644 --- a/pkg/api/records_test.go +++ b/pkg/api/records_test.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/goccy/go-json" - "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/stackitcloud/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/httpclient/default.go b/pkg/httpclient/default.go new file mode 100644 index 0000000..bf2a01f --- /dev/null +++ b/pkg/httpclient/default.go @@ -0,0 +1,54 @@ +package httpclient + +import ( + "net" + "net/http" + "time" +) + +const ( + + // DefaultUserAgent of CLI represents HTTP User-Agent header that will be added to auth requests. + DefaultUserAgent = "external-dns-selectel-webhook" + + // defaultHTTPTimeout represents the default timeout (in seconds) for HTTP requests. + defaultHTTPTimeout = 30 + + // defaultDialTimeout represents the default timeout (in seconds) for HTTP connection establishments. + defaultDialTimeout = 60 + + // defaultKeepaliveTimeout represents the default keep-alive period for an active network connection. + defaultKeepaliveTimeout = 60 + + // defaultMaxIdleConns represents the maximum number of idle (keep-alive) connections. + defaultMaxIdleConns = 100 + + // defaultIdleConnTimeout represents the maximum amount of time an idle (keep-alive) connection will remain + // idle before closing itself. + defaultIdleConnTimeout = 100 + + // defaultTLSHandshakeTimeout represents the default timeout (in seconds) for TLS handshake. + defaultTLSHandshakeTimeout = 60 + + // defaultExpectContinueTimeout represents the default amount of time to wait for a server's first + // response headers. + defaultExpectContinueTimeout = 1 +) + +// Default sets up default http client for authentication. +func Default() http.Client { + return http.Client{ + Timeout: defaultHTTPTimeout * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: defaultDialTimeout * time.Second, + KeepAlive: defaultKeepaliveTimeout * time.Second, + }).DialContext, + MaxIdleConns: defaultMaxIdleConns, + IdleConnTimeout: defaultIdleConnTimeout * time.Second, + TLSHandshakeTimeout: defaultTLSHandshakeTimeout * time.Second, + ExpectContinueTimeout: defaultExpectContinueTimeout * time.Second, + }, + } +} diff --git a/pkg/keystone/provider.go b/pkg/keystone/provider.go new file mode 100644 index 0000000..b8ef4e0 --- /dev/null +++ b/pkg/keystone/provider.go @@ -0,0 +1,72 @@ +package keystone + +import ( + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" + "github.com/selectel/external-dns-webhook/pkg/httpclient" + "go.uber.org/zap" +) + +func defaultOSClient(endpoint string) (*gophercloud.ProviderClient, error) { + client, err := openstack.NewClient(endpoint) + client.HTTPClient = httpclient.Default() + client.UserAgent.Prepend(httpclient.DefaultUserAgent) + return client, err +} + +type Credentials struct { + // IdentityEndpoint is OS_AUTH_URL variable from rc.sh. + IdentityEndpoint string + // AccountID is OS_PROJECT_DOMAIN_NAME variable from rc.sh. + AccountID string + // IdentityEndpoint is OS_PROJECT_ID variable from rc.sh. + ProjectID string + // IdentityEndpoint is OS_USERNAME variable from rc.sh. + Username string + // IdentityEndpoint is OS_PASSWORD variable from rc.sh. + Password string +} + +type Provider struct { + logger *zap.Logger + // credentials contains data to access openstack identity API. + credentials Credentials +} + +// GetToken returns valid keystone token. It will be stored for the next requests and then checked whether it is expired. +func (p Provider) GetToken() (string, error) { + p.logger.Info( + "getting keystone token", + zap.String("identity_endpoint", p.credentials.IdentityEndpoint), + zap.String("username", p.credentials.Username), + zap.String("account_id", p.credentials.AccountID), + zap.String("project_id", p.credentials.ProjectID), + ) + + opts := gophercloud.AuthOptions{ + IdentityEndpoint: p.credentials.IdentityEndpoint, + Username: p.credentials.Username, + Password: p.credentials.Password, + DomainName: p.credentials.AccountID, + Scope: &gophercloud.AuthScope{ + ProjectID: p.credentials.ProjectID, + }, + } + + p.logger.Debug("connecting to identity endpoint") + client, err := defaultOSClient(p.credentials.IdentityEndpoint) + if err != nil { + p.logger.Error("error during creating default openstack client", zap.Error(err)) + return "", err + } + err = openstack.Authenticate(client, opts) + + return client.Token(), err +} + +func NewProvider(logger *zap.Logger, credentials Credentials) *Provider { + return &Provider{ + logger: logger, + credentials: credentials, + } +} diff --git a/pkg/metrics/mock/http_middleware.go b/pkg/metrics/mock/http_middleware.go index 3ccd735..69f8943 100644 --- a/pkg/metrics/mock/http_middleware.go +++ b/pkg/metrics/mock/http_middleware.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: ./http_middleware.go +// +// Generated by this command: +// +// mockgen -destination=./mock/http_middleware.go -source=./http_middleware.go HttpApiMetrics +// // Package mock_metrics is a generated GoMock package. package mock_metrics @@ -64,7 +69,7 @@ func (m *MockHttpApiMetrics) CollectRequest(method, path string, statusCode int) } // CollectRequest indicates an expected call of CollectRequest. -func (mr *MockHttpApiMetricsMockRecorder) CollectRequest(method, path, statusCode interface{}) *gomock.Call { +func (mr *MockHttpApiMetricsMockRecorder) CollectRequest(method, path, statusCode any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CollectRequest", reflect.TypeOf((*MockHttpApiMetrics)(nil).CollectRequest), method, path, statusCode) } @@ -76,7 +81,7 @@ func (m *MockHttpApiMetrics) CollectRequestContentLength(method, path string, co } // CollectRequestContentLength indicates an expected call of CollectRequestContentLength. -func (mr *MockHttpApiMetricsMockRecorder) CollectRequestContentLength(method, path, contentLength interface{}) *gomock.Call { +func (mr *MockHttpApiMetricsMockRecorder) CollectRequestContentLength(method, path, contentLength any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CollectRequestContentLength", reflect.TypeOf((*MockHttpApiMetrics)(nil).CollectRequestContentLength), method, path, contentLength) } @@ -88,7 +93,7 @@ func (m *MockHttpApiMetrics) CollectRequestDuration(method, path string, duratio } // CollectRequestDuration indicates an expected call of CollectRequestDuration. -func (mr *MockHttpApiMetricsMockRecorder) CollectRequestDuration(method, path, duration interface{}) *gomock.Call { +func (mr *MockHttpApiMetricsMockRecorder) CollectRequestDuration(method, path, duration any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CollectRequestDuration", reflect.TypeOf((*MockHttpApiMetrics)(nil).CollectRequestDuration), method, path, duration) } @@ -100,7 +105,7 @@ func (m *MockHttpApiMetrics) CollectRequestResponseSize(method, path string, con } // CollectRequestResponseSize indicates an expected call of CollectRequestResponseSize. -func (mr *MockHttpApiMetricsMockRecorder) CollectRequestResponseSize(method, path, contentLength interface{}) *gomock.Call { +func (mr *MockHttpApiMetricsMockRecorder) CollectRequestResponseSize(method, path, contentLength any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CollectRequestResponseSize", reflect.TypeOf((*MockHttpApiMetrics)(nil).CollectRequestResponseSize), method, path, contentLength) } From 6818bc1fb78145aee2cfa4d359e9e679d1551d12 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 12:53:17 +0300 Subject: [PATCH 02/19] fix: linter issues --- cmd/webhook/cmd/root.go | 7 ++++++- internal/selprovider/apply_changes_test.go | 2 +- internal/selprovider/provider.go | 4 +++- internal/selprovider/records_test.go | 1 + internal/selprovider/rrset_fetcher.go | 1 - pkg/keystone/provider.go | 2 ++ 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cmd/webhook/cmd/root.go b/cmd/webhook/cmd/root.go index afc390d..43ce376 100644 --- a/cmd/webhook/cmd/root.go +++ b/cmd/webhook/cmd/root.go @@ -37,7 +37,12 @@ var rootCmd = &cobra.Command{ Long: "provider webhook for the Selectel DNS service", Run: func(cmd *cobra.Command, args []string) { logger := getLogger() - defer logger.Sync() + defer func(logger *zap.Logger) { + err := logger.Sync() + if err != nil { + log.Printf("Synchronization of logs failed with error: %v", err) + } + }(logger) endpointDomainFilter := endpoint.DomainFilter{Filters: domainFilter} diff --git a/internal/selprovider/apply_changes_test.go b/internal/selprovider/apply_changes_test.go index 0e4045b..210660f 100644 --- a/internal/selprovider/apply_changes_test.go +++ b/internal/selprovider/apply_changes_test.go @@ -236,7 +236,7 @@ func getChangeTypeChanges(changeType ChangeType) *plan.Changes { } } -func getApplyChangesBasicTestCases( //nolint:funlen // Test cases are long +func getApplyChangesBasicTestCases( validZoneResponse []byte, validRRSetResponse []byte, invalidZoneResponse []byte, diff --git a/internal/selprovider/provider.go b/internal/selprovider/provider.go index ef49a18..353ed39 100644 --- a/internal/selprovider/provider.go +++ b/internal/selprovider/provider.go @@ -23,11 +23,12 @@ type Provider struct { rrSetFetcherClient *rrSetFetcher } -// getDomainsClient returns v2.DNSClient with provided keystone and user-agent from httpclient.DefaultUserAgent +// getDomainsClient returns v2.DNSClient with provided keystone and user-agent from httpclient.DefaultUserAgent. func (p *Provider) getDomainsClient() (domains.DNSClient[domains.Zone, domains.RRSet], error) { token, err := p.keystoneProvider.GetToken() if err != nil { p.logger.Error("authorization error during getting keystone token", zap.Error(err)) + return nil, err } @@ -35,6 +36,7 @@ func (p *Provider) getDomainsClient() (domains.DNSClient[domains.Zone, domains.R headers := http.Header{} headers.Add("X-Auth-Token", token) headers.Add("User-Agent", httpclient.DefaultUserAgent) + return domains.NewClient(p.endpoint, &httpClient, headers), nil } diff --git a/internal/selprovider/records_test.go b/internal/selprovider/records_test.go index 7b8a6c4..183da8e 100644 --- a/internal/selprovider/records_test.go +++ b/internal/selprovider/records_test.go @@ -159,6 +159,7 @@ func TestZoneEndpoint403Records(t *testing.T) { } func getDefaultKeystoneProvider(t *testing.T, callTimes int) KeystoneProvider { + t.Helper() ctrl := gomock.NewController(t) t.Cleanup(ctrl.Finish) diff --git a/internal/selprovider/rrset_fetcher.go b/internal/selprovider/rrset_fetcher.go index cb15f0a..c73af14 100644 --- a/internal/selprovider/rrset_fetcher.go +++ b/internal/selprovider/rrset_fetcher.go @@ -12,7 +12,6 @@ import ( type rrSetFetcher struct { domainFilter endpoint.DomainFilter - projectId string logger *zap.Logger } diff --git a/pkg/keystone/provider.go b/pkg/keystone/provider.go index b8ef4e0..1709a60 100644 --- a/pkg/keystone/provider.go +++ b/pkg/keystone/provider.go @@ -11,6 +11,7 @@ func defaultOSClient(endpoint string) (*gophercloud.ProviderClient, error) { client, err := openstack.NewClient(endpoint) client.HTTPClient = httpclient.Default() client.UserAgent.Prepend(httpclient.DefaultUserAgent) + return client, err } @@ -57,6 +58,7 @@ func (p Provider) GetToken() (string, error) { client, err := defaultOSClient(p.credentials.IdentityEndpoint) if err != nil { p.logger.Error("error during creating default openstack client", zap.Error(err)) + return "", err } err = openstack.Authenticate(client, opts) From 228ef2db95d8eeefbf8a1bfa950e7ff24ddc9c3e Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 13:12:59 +0300 Subject: [PATCH 03/19] fix: renamed stackitcloud -> selectel & changed makefile due to upstream --- CONTRIBUTING.md | 37 -------------- Makefile | 8 +-- README.md | 76 +++++++++++++++------------- cmd/webhook/cmd/root.go | 10 ++-- cmd/webhook/main.go | 2 +- go.mod | 2 +- internal/selprovider/provider.go | 2 +- internal/selprovider/records_test.go | 2 +- pkg/api/adjust_endpoints_test.go | 4 +- pkg/api/api.go | 2 +- pkg/api/api_test.go | 4 +- pkg/api/apply_changes_test.go | 4 +- pkg/api/domain_filter_test.go | 4 +- pkg/api/health_test.go | 4 +- pkg/api/metrics.go | 2 +- pkg/api/metrics_test.go | 6 +-- pkg/api/records_test.go | 4 +- pkg/keystone/provider.go | 2 +- 18 files changed, 71 insertions(+), 104 deletions(-) delete mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index cd41e9b..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,37 +0,0 @@ -# Contributing to External DNS STACKIT Webhook - -Welcome and thank you for making it this far and considering contributing to external-dns-stackit-webhook. -We always appreciate any contributions by raising issues, improving the documentation, fixing bugs in the CLI or adding new features. - -Before opening a PR please read through this document. -If you want to contribute but don't know how to start or have any questions feel free to reach out to us on [Github Discussions](https://github.com/stackitcloud/external-dns-stackit-webhook/discussions). Answering any questions or discussions there is also a great way to contribute to the community. - -## Process of making an addition - -> Please keep in mind to open an issue whenever you plan to make an addition to features to discuss it before implementing it. - -To contribute any code to this repository just do the following: - -1. Make sure you have Go's latest version installed -2. Fork this repository -3. Run `make build` to make sure everything's setup correctly -4. Make your changes - > Please follow the [seven rules of greate Git commit messages](https://chris.beams.io/posts/git-commit/#seven-rules) - > and make sure to keep your commits clean and atomic. - > Your PR won't be squashed before merging so the commits should tell a story. - > - > Optional: Sign-off on all Git commits by running `git commit -s`. - > Take a look at the [Gihub Docs](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) for further information. - > - > Add documentation and tests for your addition if needed. -5. Run `make lint test` to ensure your code is ready to be merged - > If any linting issues occur please fix them. - > Using a nolint directive should only be used as a last resort. -6. Open a PR and make sure the CI pipelines succeed. - > Your PR needs to have a semantic title, which can look like: `type(scope) Short Description` - > All available `scopes` & `types` are defined in [semantic.yml](https://github.com/stackitcloud/external-dns-stackit-webhook/blob/main/.github/semantic.yml) - > - > A example PR tile for adding a new feature for the CLI would looks like: `cli(feat) Add saving output to file` -7. Wait for one of the maintainers to review your code and react to the comments. -8. After approval merge the PR -9. Thank you for your contribution! :) diff --git a/Makefile b/Makefile index e191246..5142615 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,8 @@ GOLANGCI_VERSION = 1.53.3 LICENCES_IGNORE_LIST = $(shell cat licenses/licenses-ignore-list.txt) VERSION ?= 0.0.1 -IMAGE_TAG_BASE ?= selectel/external-dns-webhook -IMG ?= $(IMAGE_TAG_BASE) +IMAGE_TAG_BASE ?= selectel/external-dns-stackit-webhook +IMG ?= $(IMAGE_TAG_BASE):$(VERSION) BUILD_VERSION ?= $(shell git branch --show-current) BUILD_COMMIT ?= $(shell git rev-parse --short HEAD) @@ -17,7 +17,7 @@ download: .PHONY: build build: - CGO_ENABLED=0 go build -ldflags "-s -w" -o ./external-dns-webhook -v cmd/webhook/main.go + CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/external-dns-stackit-webhook -v cmd/webhook/main.go .PHONY: docker-build docker-build: @@ -79,4 +79,4 @@ license-check: $(GO_LICENSES) reports ## Check licenses against code. .PHONY: license-report license-report: $(GO_LICENSES) reports ## Create licenses report against code. - $(GO_LICENSES) report --include_tests --ignore $(LICENCES_IGNORE_LIST) ./... > ./reports/licenses/licenses-list.csv + $(GO_LICENSES) report --include_tests --ignore $(LICENCES_IGNORE_LIST) ./... > ./reports/licenses/licenses-list.csv \ No newline at end of file diff --git a/README.md b/README.md index a8839f9..c77901d 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,37 @@ -# STACKIT Webhook - ExternalDNS +# Selectel Webhook - ExternalDNS [![GoTemplate](https://img.shields.io/badge/go/template-black?logo=go)](https://github.com/golang-standards/project-layout) -[![CI](https://github.com/stackitcloud/external-dns-stackit-webhook/actions/workflows/main.yml/badge.svg)](https://github.com/stackitcloud/external-dns-stackit-webhook/actions/workflows/main.yml) -[![Go Report Card](https://goreportcard.com/badge/github.com/stackitcloud/external-dns-stackit-webhook)](https://goreportcard.com/report/github.com/stackitcloud/external-dns-stackit-webhook) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![GitHub release](https://img.shields.io/github/release/stackitcloud/external-dns-stackit-webhook.svg)](https://github.com/stackitcloud/external-dns-stackit-webhook/releases) -[![Last Commit](https://img.shields.io/github/last-commit/stackitcloud/external-dns-stackit-webhook/main.svg)](https://github.com/stackitcloud/external-dns-stackit-webhook/commits/main) -[![GitHub issues](https://img.shields.io/github/issues/stackitcloud/external-dns-stackit-webhook.svg)](https://github.com/stackitcloud/external-dns-stackit-webhook/issues) -[![GitHub pull requests](https://img.shields.io/github/issues-pr/stackitcloud/external-dns-stackit-webhook.svg)](https://github.com/stackitcloud/external-dns-stackit-webhook/pulls) -[![GitHub stars](https://img.shields.io/github/stars/stackitcloud/external-dns-stackit-webhook.svg?style=social&label=Star&maxAge=2592000)](https://github.com/stackitcloud/external-dns-stackit-webhook/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/stackitcloud/external-dns-stackit-webhook.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/stackitcloud/external-dns-stackit-webhook/network) +[![CI](https://github.com/selectel/external-dns-stackit-webhook/actions/workflows/main.yml/badge.svg)](https://github.com/selectel/external-dns-stackit-webhook/actions/workflows/main.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/selectel/external-dns-stackit-webhook)](https://goreportcard.com/report/github.com/selectel/external-dns-stackit-webhook) +[![GitHub release](https://img.shields.io/github/release/selectel/external-dns-stackit-webhook.svg)](https://github.com/selectel/external-dns-stackit-webhook/releases) +[![Last Commit](https://img.shields.io/github/last-commit/selectel/external-dns-stackit-webhook/main.svg)](https://github.com/selectel/external-dns-stackit-webhook/commits/main) +[![GitHub issues](https://img.shields.io/github/issues/selectel/external-dns-stackit-webhook.svg)](https://github.com/selectel/external-dns-stackit-webhook/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/selectel/external-dns-stackit-webhook.svg)](https://github.com/selectel/external-dns-stackit-webhook/pulls) +[![GitHub stars](https://img.shields.io/github/stars/selectel/external-dns-stackit-webhook.svg?style=social&label=Star&maxAge=2592000)](https://github.com/selectel/external-dns-stackit-webhook/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/selectel/external-dns-stackit-webhook.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/selectel/external-dns-stackit-webhook/network) ExternalDNS serves as an add-on for Kubernetes designed to automate the management of Domain Name System (DNS) records for Kubernetes services by utilizing various DNS providers. While Kubernetes traditionally manages DNS records internally, ExternalDNS augments this functionality by transferring the responsibility of DNS records -management to an external DNS provider such as STACKIT. Consequently, the STACKIT webhook enables the management -of your STACKIT domains within your Kubernetes cluster using +management to an external DNS provider such as Selectel. Consequently, the Selectel webhook enables the management +of your Selectel domains within your Kubernetes cluster using [ExternalDNS](https://github.com/kubernetes-sigs/external-dns). -For utilizing ExternalDNS with STACKIT, it is mandatory to establish a STACKIT project, a service account +For utilizing ExternalDNS with Selectel, it is mandatory to establish a Selectel project, a service account within the project, generate an authentication token for the service account, authorize the service account -to create and read dns zones, and finally, establish a STACKIT zone. +to create and read dns zones, and finally, establish a Selectel zone. ## Kubernetes Deployment -The STACKIT webhook is presented as a standard Open Container Initiative (OCI) image released in the -[GitHub container registry](https://github.com/stackitcloud/external-dns-stackit-webhook/pkgs/container/external-dns-stackit-webhook). +The Selectel webhook is presented as a standard Open Container Initiative (OCI) image released in the +[GitHub container registry](https://github.com/selectel/external-dns-stackit-webhook/pkgs/container/external-dns-stackit-webhook). The deployment is compatible with all Kubernetes-supported methods. The subsequent example demonstrates the deployment as a [sidecar container](https://kubernetes.io/docs/concepts/workloads/pods/#workload-resources-for-managing-pods) within the ExternalDNS pod. ```shell -kubectl create secret generic external-dns-stackit-webhook --from-literal=auth-token='' +kubectl create secret generic external-dns-webhook --from-literal=password='' ``` ```shell @@ -176,10 +175,12 @@ spec: readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65534 - image: ghcr.io/stackitcloud/external-dns-stackit-webhook:v0.2.0 + image: ghcr.io/selectel/external-dns-stackit-webhook:v0.1.0 imagePullPolicy: IfNotPresent args: - --project-id=c158c736-0300-4044-95c4-b7d404279b35 # your project id + - --account-id=000000 # your account id + - --username=Username # your service user's name ports: - name: http protocol: TCP @@ -203,26 +204,28 @@ spec: successThreshold: 1 timeoutSeconds: 5 env: - - name: AUTH_TOKEN + - name: PASSWORD valueFrom: secretKeyRef: - name: external-dns-stackit-webhook - key: auth-token + name: external-dns-webhook + key: password EOF ``` ## Configuration -The configuration of the STACKIT webhook can be accomplished through command line arguments and environment variables. +The configuration of the Selectel webhook can be accomplished through command line arguments and environment variables. Below are the options that are available. -- `--project-id`/`PROJECT_ID` (required): Specifies the project id of the STACKIT project. -- `--auth-token`/`AUTH_TOKEN` (required): Defines the authentication token for the STACKIT API. +- `--project-id`/`PROJECT_ID` (required): Specifies the project id to authorize. +- `--account-id`/`ACCOUNT_ID` (required): Specifies the account id to authorize. +- `--username`/`USERNAME` (required): Specifies the username of your service user to authorize. +- `--password`/`PASSWORD` (required): Specifies the password of your service user to authorize. - `--worker`/`WORKER` (optional): Specifies the number of workers to employ for querying the API. Given that we need to iterate over all zones and records, it can be parallelized. However, it is important to avoid setting this number excessively high to prevent receiving 429 rate limiting from the API (default 10). - `--base-url`/`BASE_URL` (optional): Identifies the Base URL for utilizing the API ( - default "https://dns.api.stackit.cloud"). + default "https://api.selectel.ru/domains/v2"). - `--api-port`/`API_PORT` (optional): Specifies the port to listen on (default 8888). - `--domain-filter`/`DOMAIN_FILER` (optional): Establishes a filter for DNS zone names (default []). - `--dry-run`/`DRY_RUN` (optional): Specifies whether to perform a dry run (default false). @@ -233,7 +236,7 @@ Below are the options that are available. ### 1. Issue with Creating Service using External DNS Annotation -If your zone is `example.runs.onstackit.cloud` and you're trying to create a service with the following external DNS +If your zone is `example.runs.selcloud.ru` and you're trying to create a service with the following external DNS annotation: ```yaml @@ -241,7 +244,7 @@ annotation: kind: Service metadata: annotations: - external-dns.alpha.kubernetes.io/hostname: example.runs.onstackit.cloud + external-dns.alpha.kubernetes.io/hostname: example.runs.selcloud.ru labels: app.kubernetes.io/name: ingress-nginx app.kubernetes.io/instance: nginx @@ -272,13 +275,13 @@ annotation: Why isn't it working? -Answer: The External DNS will try to create a TXT record named `a-example.runs.onstackit.cloud`, which will fail +Answer: The External DNS will try to create a TXT record named `a-example.runs.selcloud.ru`, which will fail because you can't establish a record outside the zone. The solution is to use a name that's within the zone, such as -`nginx.example.runs.onstackit.cloud`. +`nginx.example.runs.selcloud.ru`. ### 2. Issues with Creating Ingresses not in the Zone -For a project containing the zone `example.runs.onstackit.cloud`, suppose you've created these two ingress: +For a project containing the zone `example.runs.selcloud.ru`, suppose you've created these two ingress: ```yaml apiVersion: networking.k8s.io/v1 @@ -291,7 +294,7 @@ For a project containing the zone `example.runs.onstackit.cloud`, suppose you've namespace: default spec: rules: - - host: test.example.runs.onstackit.cloud + - host: test.example.runs.selcloud.ru http: paths: - backend: @@ -319,19 +322,20 @@ For a project containing the zone `example.runs.onstackit.cloud`, suppose you've `example.stackit.rocks` isn't within the project, it'll fail. There are two potential fixes: - Incorporate the zone `example.stackit.rocks` into the project. -- Adjust the domain filter to `example.runs.onstackit.cloud` by setting the domain filter - flag `--domain-filter="example.runs.onstackit.cloud"`. This will exclude `test.example.stackit.rocks` and only +- Adjust the domain filter to `example.runs.selcloud.ru` by setting the domain filter + flag `--domain-filter="example.runs.selcloud.ru"`. This will exclude `test.example.stackit.rocks` and only generate - the record set for `test.example.runs.onstackit.cloud`. + the record set for `test.example.runs.selcloud.ru`. ## Development Run the app: ```bash -export BASE_URL="https://dns.api.stackit.cloud" export PROJECT_ID="c158c736-0300-4044-95c4-b7d404279b35" -export AUTH_TOKEN="your-auth-token" +export ACCOUNT_ID="123456" +export USERNAME="username" +export PASSWORD ="password" make run ``` diff --git a/cmd/webhook/cmd/root.go b/cmd/webhook/cmd/root.go index 43ce376..a98c56e 100644 --- a/cmd/webhook/cmd/root.go +++ b/cmd/webhook/cmd/root.go @@ -5,10 +5,10 @@ import ( "log" "strings" - "github.com/selectel/external-dns-webhook/internal/selprovider" - "github.com/selectel/external-dns-webhook/pkg/api" - "github.com/selectel/external-dns-webhook/pkg/keystone" - "github.com/selectel/external-dns-webhook/pkg/metrics" + "github.com/selectel/external-dns-stackit-webhook/internal/selprovider" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + "github.com/selectel/external-dns-stackit-webhook/pkg/keystone" + "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -32,7 +32,7 @@ var ( ) var rootCmd = &cobra.Command{ - Use: "external-dns-selectel-webhook", + Use: "external-dns-stackit-webhook", Short: "provider webhook for the Selectel DNS service", Long: "provider webhook for the Selectel DNS service", Run: func(cmd *cobra.Command, args []string) { diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 0ba2ba2..95a48e3 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -1,6 +1,6 @@ package main -import "github.com/selectel/external-dns-webhook/cmd/webhook/cmd" +import "github.com/selectel/external-dns-stackit-webhook/cmd/webhook/cmd" func main() { err := cmd.Execute() diff --git a/go.mod b/go.mod index 7290a0c..c45e6f9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/selectel/external-dns-webhook +module github.com/selectel/external-dns-stackit-webhook go 1.20 diff --git a/internal/selprovider/provider.go b/internal/selprovider/provider.go index 353ed39..ce5437f 100644 --- a/internal/selprovider/provider.go +++ b/internal/selprovider/provider.go @@ -4,7 +4,7 @@ import ( "net/http" domains "github.com/selectel/domains-go/pkg/v2" - "github.com/selectel/external-dns-webhook/pkg/httpclient" + "github.com/selectel/external-dns-stackit-webhook/pkg/httpclient" "go.uber.org/zap" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/provider" diff --git a/internal/selprovider/records_test.go b/internal/selprovider/records_test.go index 183da8e..f20c41a 100644 --- a/internal/selprovider/records_test.go +++ b/internal/selprovider/records_test.go @@ -9,7 +9,7 @@ import ( "github.com/goccy/go-json" domains "github.com/selectel/domains-go/pkg/v2" - mock_selprovider "github.com/selectel/external-dns-webhook/internal/selprovider/mock" + mock_selprovider "github.com/selectel/external-dns-stackit-webhook/internal/selprovider/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/adjust_endpoints_test.go b/pkg/api/adjust_endpoints_test.go index 78cd26e..311a90d 100644 --- a/pkg/api/adjust_endpoints_test.go +++ b/pkg/api/adjust_endpoints_test.go @@ -8,8 +8,8 @@ import ( "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/api.go b/pkg/api/api.go index 2424570..ef6a90c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -15,7 +15,7 @@ import ( fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/pprof" fiberrecover "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/selectel/external-dns-webhook/pkg/metrics" + "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" "go.uber.org/zap" "sigs.k8s.io/external-dns/provider" ) diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index c2c4968..ed5a6f4 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/selectel/external-dns-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/apply_changes_test.go b/pkg/api/apply_changes_test.go index fc4208b..917149f 100644 --- a/pkg/api/apply_changes_test.go +++ b/pkg/api/apply_changes_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/domain_filter_test.go b/pkg/api/domain_filter_test.go index 0985ea2..24a031b 100644 --- a/pkg/api/domain_filter_test.go +++ b/pkg/api/domain_filter_test.go @@ -8,8 +8,8 @@ import ( "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/health_test.go b/pkg/api/health_test.go index 6062081..ea6093b 100644 --- a/pkg/api/health_test.go +++ b/pkg/api/health_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index bf12957..b50c449 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -7,7 +7,7 @@ import ( "github.com/gofiber/adaptor/v2" "github.com/gofiber/fiber/v2" "github.com/prometheus/client_golang/prometheus/promhttp" - metrics_collector "github.com/selectel/external-dns-webhook/pkg/metrics" + metrics_collector "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" ) // registerAt registers the metrics endpoint. diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 994bbff..dc11cab 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-webhook/pkg/api" - metrics_collector "github.com/selectel/external-dns-webhook/pkg/metrics" - mock_metrics_collector "github.com/selectel/external-dns-webhook/pkg/metrics/mock" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + metrics_collector "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" + mock_metrics_collector "github.com/selectel/external-dns-stackit-webhook/pkg/metrics/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) diff --git a/pkg/api/records_test.go b/pkg/api/records_test.go index a57c94c..39b4a40 100644 --- a/pkg/api/records_test.go +++ b/pkg/api/records_test.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/goccy/go-json" - "github.com/selectel/external-dns-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-webhook/pkg/api/mock" + "github.com/selectel/external-dns-stackit-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/keystone/provider.go b/pkg/keystone/provider.go index 1709a60..d5ca68b 100644 --- a/pkg/keystone/provider.go +++ b/pkg/keystone/provider.go @@ -3,7 +3,7 @@ package keystone import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" - "github.com/selectel/external-dns-webhook/pkg/httpclient" + "github.com/selectel/external-dns-stackit-webhook/pkg/httpclient" "go.uber.org/zap" ) From c832b78b06e2a99b66d6243e8536e9f9f3bb8eda Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 13:14:08 +0300 Subject: [PATCH 04/19] refactor: removed stackitprovider --- internal/stackitprovider/apply_changes.go | 258 ------------ .../stackitprovider/apply_changes_test.go | 360 ---------------- internal/stackitprovider/config.go | 11 - internal/stackitprovider/domain_filter.go | 7 - .../stackitprovider/domain_filter_test.go | 21 - internal/stackitprovider/helper.go | 113 ----- internal/stackitprovider/helper_test.go | 216 ---------- internal/stackitprovider/models.go | 26 -- internal/stackitprovider/records.go | 104 ----- internal/stackitprovider/records_test.go | 395 ------------------ internal/stackitprovider/rrset_fetcher.go | 99 ----- internal/stackitprovider/stackit.go | 47 --- internal/stackitprovider/stackit_test.go | 1 - internal/stackitprovider/zone_fetcher.go | 84 ---- 14 files changed, 1742 deletions(-) delete mode 100644 internal/stackitprovider/apply_changes.go delete mode 100644 internal/stackitprovider/apply_changes_test.go delete mode 100644 internal/stackitprovider/config.go delete mode 100644 internal/stackitprovider/domain_filter.go delete mode 100644 internal/stackitprovider/domain_filter_test.go delete mode 100644 internal/stackitprovider/helper.go delete mode 100644 internal/stackitprovider/helper_test.go delete mode 100644 internal/stackitprovider/models.go delete mode 100644 internal/stackitprovider/records.go delete mode 100644 internal/stackitprovider/records_test.go delete mode 100644 internal/stackitprovider/rrset_fetcher.go delete mode 100644 internal/stackitprovider/stackit.go delete mode 100644 internal/stackitprovider/stackit_test.go delete mode 100644 internal/stackitprovider/zone_fetcher.go diff --git a/internal/stackitprovider/apply_changes.go b/internal/stackitprovider/apply_changes.go deleted file mode 100644 index 9b89309..0000000 --- a/internal/stackitprovider/apply_changes.go +++ /dev/null @@ -1,258 +0,0 @@ -package stackitprovider - -import ( - "context" - "fmt" - - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "go.uber.org/zap" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" -) - -// ApplyChanges applies a given set of changes in a given zone. -func (d *StackitDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - // create rr set. POST /v1/projects/{projectId}/zones/{zoneId}/rrsets - err := d.createRRSets(ctx, changes.Create) - if err != nil { - return err - } - - // update rr set. PATCH /v1/projects/{projectId}/zones/{zoneId}/rrsets/{rrSetId} - err = d.updateRRSets(ctx, changes.UpdateNew) - if err != nil { - return err - } - - // delete rr set. DELETE /v1/projects/{projectId}/zones/{zoneId}/rrsets/{rrSetId} - err = d.deleteRRSets(ctx, changes.Delete) - if err != nil { - return err - } - - return nil -} - -// createRRSets creates new record sets in the stackitprovider for the given endpoints that are in the -// creation field. -func (d *StackitDNSProvider) createRRSets( - ctx context.Context, - endpoints []*endpoint.Endpoint, -) error { - if len(endpoints) == 0 { - return nil - } - - zones, err := d.zoneFetcherClient.zones(ctx) - if err != nil { - return err - } - - return d.handleRRSetWithWorkers(ctx, endpoints, zones, CREATE) -} - -// createRRSet creates a new record set in the stackitprovider for the given endpoint. -func (d *StackitDNSProvider) createRRSet( - ctx context.Context, - change *endpoint.Endpoint, - zones []stackitdnsclient.Zone, -) error { - resultZone, found := findBestMatchingZone(change.DNSName, zones) - if !found { - return fmt.Errorf("no matching zone found for %s", change.DNSName) - } - - logFields := getLogFields(change, CREATE, *resultZone.Id) - d.logger.Info("create record set", logFields...) - - if d.dryRun { - d.logger.Debug("dry run, skipping", logFields...) - - return nil - } - - modifyChange(change) - - rrSetPayload := getStackitRecordSetPayload(change) - - // ignore all errors to just retry on next run - _, err := d.apiClient.CreateRecordSet(ctx, d.projectId, *resultZone.Id).CreateRecordSetPayload(rrSetPayload).Execute() - if err != nil { - d.logger.Error("error creating record set", zap.Error(err)) - - return err - } - - d.logger.Info("create record set successfully", logFields...) - - return nil -} - -// updateRRSets patches (overrides) contents in the record sets in the stackitprovider for the given -// endpoints that are in the update new field. -func (d *StackitDNSProvider) updateRRSets( - ctx context.Context, - endpoints []*endpoint.Endpoint, -) error { - if len(endpoints) == 0 { - return nil - } - - zones, err := d.zoneFetcherClient.zones(ctx) - if err != nil { - return err - } - - return d.handleRRSetWithWorkers(ctx, endpoints, zones, UPDATE) -} - -// updateRRSet patches (overrides) contents in the record set in the stackitprovider. -func (d *StackitDNSProvider) updateRRSet( - ctx context.Context, - change *endpoint.Endpoint, - zones []stackitdnsclient.Zone, -) error { - modifyChange(change) - - resultZone, resultRRSet, err := d.rrSetFetcherClient.getRRSetForUpdateDeletion(ctx, change, zones) - if err != nil { - return err - } - - logFields := getLogFields(change, UPDATE, *resultRRSet.Id) - d.logger.Info("update record set", logFields...) - - if d.dryRun { - d.logger.Debug("dry run, skipping", logFields...) - - return nil - } - - rrSet := getStackitPartialUpdateRecordSetPayload(change) - - _, err = d.apiClient.PartialUpdateRecordSet(ctx, d.projectId, *resultZone.Id, *resultRRSet.Id).PartialUpdateRecordSetPayload(rrSet).Execute() - if err != nil { - d.logger.Error("error updating record set", zap.Error(err)) - - return err - } - - d.logger.Info("update record set successfully", logFields...) - - return nil -} - -// deleteRRSets deletes record sets in the stackitprovider for the given endpoints that are in the -// deletion field. -func (d *StackitDNSProvider) deleteRRSets( - ctx context.Context, - endpoints []*endpoint.Endpoint, -) error { - if len(endpoints) == 0 { - d.logger.Debug("no endpoints to delete") - - return nil - } - - d.logger.Info("records to delete", zap.String("records", fmt.Sprintf("%v", endpoints))) - - zones, err := d.zoneFetcherClient.zones(ctx) - if err != nil { - return err - } - - return d.handleRRSetWithWorkers(ctx, endpoints, zones, DELETE) -} - -// deleteRRSet deletes a record set in the stackitprovider for the given endpoint. -func (d *StackitDNSProvider) deleteRRSet( - ctx context.Context, - change *endpoint.Endpoint, - zones []stackitdnsclient.Zone, -) error { - modifyChange(change) - - resultZone, resultRRSet, err := d.rrSetFetcherClient.getRRSetForUpdateDeletion(ctx, change, zones) - if err != nil { - return err - } - - logFields := getLogFields(change, DELETE, *resultRRSet.Id) - d.logger.Info("delete record set", logFields...) - - if d.dryRun { - d.logger.Debug("dry run, skipping", logFields...) - - return nil - } - - _, err = d.apiClient.DeleteRecordSet(ctx, d.projectId, *resultZone.Id, *resultRRSet.Id).Execute() - if err != nil { - d.logger.Error("error deleting record set", zap.Error(err)) - - return err - } - - d.logger.Info("delete record set successfully", logFields...) - - return nil -} - -// handleRRSetWithWorkers handles the given endpoints with workers to optimize speed. -func (d *StackitDNSProvider) handleRRSetWithWorkers( - ctx context.Context, - endpoints []*endpoint.Endpoint, - zones []stackitdnsclient.Zone, - action string, -) error { - workerChannel := make(chan changeTask, len(endpoints)) - errorChannel := make(chan error, len(endpoints)) - - for i := 0; i < d.workers; i++ { - go d.changeWorker(ctx, workerChannel, errorChannel, zones) - } - - for _, change := range endpoints { - workerChannel <- changeTask{ - action: action, - change: change, - } - } - - for i := 0; i < len(endpoints); i++ { - err := <-errorChannel - if err != nil { - close(workerChannel) - - return err - } - } - - close(workerChannel) - - return nil -} - -// changeWorker is a worker that handles changes passed by a channel. -func (d *StackitDNSProvider) changeWorker( - ctx context.Context, - changes chan changeTask, - errorChannel chan error, - zones []stackitdnsclient.Zone, -) { - for change := range changes { - switch change.action { - case CREATE: - err := d.createRRSet(ctx, change.change, zones) - errorChannel <- err - case UPDATE: - err := d.updateRRSet(ctx, change.change, zones) - errorChannel <- err - case DELETE: - err := d.deleteRRSet(ctx, change.change, zones) - errorChannel <- err - } - } - - d.logger.Debug("change worker finished") -} diff --git a/internal/stackitprovider/apply_changes_test.go b/internal/stackitprovider/apply_changes_test.go deleted file mode 100644 index 769e72e..0000000 --- a/internal/stackitprovider/apply_changes_test.go +++ /dev/null @@ -1,360 +0,0 @@ -package stackitprovider - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stretchr/testify/assert" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/plan" -) - -type ChangeType int - -const ( - Create ChangeType = iota - Update - Delete -) - -func TestApplyChanges(t *testing.T) { - t.Parallel() - - testingData := []struct { - changeType ChangeType - }{ - {changeType: Create}, - {changeType: Update}, - {changeType: Delete}, - } - - for _, data := range testingData { - testApplyChanges(t, data.changeType) - } -} - -func testApplyChanges(t *testing.T, changeType ChangeType) { - t.Helper() - ctx := context.Background() - validZoneResponse := getValidResponseZoneAllBytes(t) - validRRSetResponse := getValidResponseRRSetAllBytes(t) - invalidZoneResponse := []byte(`{"invalid: "json"`) - - // Test cases - tests := getApplyChangesBasicTestCases(validZoneResponse, validRRSetResponse, invalidZoneResponse) - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - - // Set up common endpoint for all types of changes - setUpCommonEndpoints(mux, tt.responseZone, tt.responseZoneCode) - - // Set up change type-specific endpoints - setUpChangeTypeEndpoints(t, mux, tt.responseRrset, tt.responseRrsetCode, changeType) - - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - // Set up the changes according to the change type - changes := getChangeTypeChanges(changeType) - - err = stackitDnsProvider.ApplyChanges(ctx, changes) - if tt.expectErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} - -func TestNoMatchingZoneFound(t *testing.T) { - t.Parallel() - - ctx := context.Background() - validZoneResponse := getValidResponseZoneAllBytes(t) - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - defer server.Close() - - // Set up common endpoint for all types of changes - setUpCommonEndpoints(mux, validZoneResponse, http.StatusOK) - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - changes := &plan.Changes{ - Create: []*endpoint.Endpoint{ - {DNSName: "notfound.com", Targets: endpoint.Targets{"test.notfound.com"}}, - }, - UpdateNew: []*endpoint.Endpoint{}, - Delete: []*endpoint.Endpoint{}, - } - - err = stackitDnsProvider.ApplyChanges(ctx, changes) - assert.Error(t, err) -} - -func TestNoRRSetFound(t *testing.T) { - t.Parallel() - - ctx := context.Background() - validZoneResponse := getValidResponseZoneAllBytes(t) - rrSets := getValidResponseRRSetAll() - rrSet := *rrSets.RrSets - *rrSet[0].Name = "notfound.test.com" - validRRSetResponse, err := json.Marshal(rrSets) - assert.NoError(t, err) - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - defer server.Close() - - // Set up common endpoint for all types of changes - setUpCommonEndpoints(mux, validZoneResponse, http.StatusOK) - - mux.HandleFunc( - "/v1/projects/1234/zones/1234/rrsets", - responseHandler(validRRSetResponse, http.StatusOK), - ) - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - changes := &plan.Changes{ - UpdateNew: []*endpoint.Endpoint{ - {DNSName: "test.com", Targets: endpoint.Targets{"notfound.test.com"}}, - }, - } - - err = stackitDnsProvider.ApplyChanges(ctx, changes) - assert.Error(t, err) -} - -// setUpCommonEndpoints for all change types. -func setUpCommonEndpoints(mux *http.ServeMux, responseZone []byte, responseZoneCode int) { - mux.HandleFunc("/v1/projects/1234/zones", func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(responseZoneCode) - w.Write(responseZone) - }) -} - -// setUpChangeTypeEndpoints for type-specific endpoints. -func setUpChangeTypeEndpoints( - t *testing.T, - mux *http.ServeMux, - responseRrset []byte, - responseRrsetCode int, - changeType ChangeType, -) { - t.Helper() - - switch changeType { - case Create: - mux.HandleFunc( - "/v1/projects/1234/zones/1234/rrsets", - responseHandler(responseRrset, responseRrsetCode), - ) - case Update, Delete: - mux.HandleFunc( - "/v1/projects/1234/zones/1234/rrsets/1234", - responseHandler(nil, responseRrsetCode), - ) - mux.HandleFunc( - "/v1/projects/1234/zones/1234/rrsets", - func(w http.ResponseWriter, r *http.Request) { - getRrsetsResponseRecordsNonPaged(t, w, "1234") - }, - ) - mux.HandleFunc( - "/v1/projects/1234/zones/5678/rrsets", - func(w http.ResponseWriter, r *http.Request) { - getRrsetsResponseRecordsNonPaged(t, w, "5678") - }, - ) - } -} - -// getChangeTypeChanges according to the change type. -func getChangeTypeChanges(changeType ChangeType) *plan.Changes { - switch changeType { - case Create: - return &plan.Changes{ - Create: []*endpoint.Endpoint{ - {DNSName: "test.com", Targets: endpoint.Targets{"test.test.com"}}, - }, - UpdateNew: []*endpoint.Endpoint{}, - Delete: []*endpoint.Endpoint{}, - } - case Update: - return &plan.Changes{ - UpdateNew: []*endpoint.Endpoint{ - {DNSName: "test.com", Targets: endpoint.Targets{"test.com"}, RecordType: "A"}, - }, - } - case Delete: - return &plan.Changes{ - Delete: []*endpoint.Endpoint{ - {DNSName: "test.com", Targets: endpoint.Targets{"test.com"}, RecordType: "A"}, - }, - } - default: - return nil - } -} - -func getApplyChangesBasicTestCases( - validZoneResponse []byte, - validRRSetResponse []byte, - invalidZoneResponse []byte, -) []struct { - name string - responseZone []byte - responseZoneCode int - responseRrset []byte - responseRrsetCode int - expectErr bool - expectedRrsetMethod string -} { - tests := []struct { - name string - responseZone []byte - responseZoneCode int - responseRrset []byte - responseRrsetCode int - expectErr bool - expectedRrsetMethod string - }{ - { - "Valid response", - validZoneResponse, - http.StatusOK, - validRRSetResponse, - http.StatusAccepted, - false, - http.MethodPost, - }, - { - "Zone response 403", - nil, - http.StatusForbidden, - validRRSetResponse, - http.StatusAccepted, - true, - "", - }, - { - "Zone response 500", - nil, - http.StatusInternalServerError, - validRRSetResponse, - http.StatusAccepted, - true, - "", - }, - { - "Zone response Invalid JSON", - invalidZoneResponse, - http.StatusOK, - validRRSetResponse, - http.StatusAccepted, - true, - "", - }, - { - "Zone response, Rrset response 403", - validZoneResponse, - http.StatusOK, - nil, - http.StatusForbidden, - true, - http.MethodPost, - }, - { - "Zone response, Rrset response 500", - validZoneResponse, - http.StatusOK, - nil, - http.StatusInternalServerError, - true, - http.MethodPost, - }, - } - - return tests -} - -func responseHandler(responseBody []byte, statusCode int) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(statusCode) - if responseBody != nil { - w.Write(responseBody) - } - } -} - -func getValidResponseZoneAllBytes(t *testing.T) []byte { - t.Helper() - - zones := getValidZoneResponseAll() - validZoneResponse, err := json.Marshal(zones) - assert.NoError(t, err) - - return validZoneResponse -} - -func getValidZoneResponseAll() stackitdnsclient.ListZonesResponse { - return stackitdnsclient.ListZonesResponse{ - ItemsPerPage: pointerTo(int64(10)), - Message: pointerTo("success"), - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(1)), - Zones: &[]stackitdnsclient.Zone{ - {Id: pointerTo("1234"), DnsName: pointerTo("test.com")}, - {Id: pointerTo("5678"), DnsName: pointerTo("test2.com")}, - }, - } -} - -func getValidResponseRRSetAllBytes(t *testing.T) []byte { - t.Helper() - - rrSets := getValidResponseRRSetAll() - validRRSetResponse, err := json.Marshal(rrSets) - assert.NoError(t, err) - - return validRRSetResponse -} - -func getValidResponseRRSetAll() stackitdnsclient.ListRecordSetsResponse { - return stackitdnsclient.ListRecordSetsResponse{ - ItemsPerPage: pointerTo(int64(20)), - Message: pointerTo("success"), - RrSets: &[]stackitdnsclient.RecordSet{ - { - Name: pointerTo("test.com"), - Type: pointerTo("A"), - Ttl: pointerTo(int64(300)), - Records: &[]stackitdnsclient.Record{ - {Content: pointerTo("1.2.3.4")}, - }, - }, - }, - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(1)), - } -} diff --git a/internal/stackitprovider/config.go b/internal/stackitprovider/config.go deleted file mode 100644 index 604c2d9..0000000 --- a/internal/stackitprovider/config.go +++ /dev/null @@ -1,11 +0,0 @@ -package stackitprovider - -import "sigs.k8s.io/external-dns/endpoint" - -// Config is used to configure the creation of the StackitDNSProvider. -type Config struct { - ProjectId string - DomainFilter endpoint.DomainFilter - DryRun bool - Workers int -} diff --git a/internal/stackitprovider/domain_filter.go b/internal/stackitprovider/domain_filter.go deleted file mode 100644 index a0edb25..0000000 --- a/internal/stackitprovider/domain_filter.go +++ /dev/null @@ -1,7 +0,0 @@ -package stackitprovider - -import "sigs.k8s.io/external-dns/endpoint" - -func (d *StackitDNSProvider) GetDomainFilter() endpoint.DomainFilter { - return d.domainFilter -} diff --git a/internal/stackitprovider/domain_filter_test.go b/internal/stackitprovider/domain_filter_test.go deleted file mode 100644 index 0680e0d..0000000 --- a/internal/stackitprovider/domain_filter_test.go +++ /dev/null @@ -1,21 +0,0 @@ -package stackitprovider - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "sigs.k8s.io/external-dns/endpoint" -) - -func TestGetDomainFilter(t *testing.T) { - t.Parallel() - - server := getServerRecords(t) - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - domainFilter := stackitDnsProvider.GetDomainFilter() - assert.Equal(t, domainFilter, endpoint.DomainFilter{}) -} diff --git a/internal/stackitprovider/helper.go b/internal/stackitprovider/helper.go deleted file mode 100644 index 491fe8c..0000000 --- a/internal/stackitprovider/helper.go +++ /dev/null @@ -1,113 +0,0 @@ -package stackitprovider - -import ( - "strings" - - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "go.uber.org/zap" - "sigs.k8s.io/external-dns/endpoint" -) - -// findBestMatchingZone finds the best matching zone for a given record set name. The criteria are -// that the zone name is contained in the record set name and that the zone name is the longest -// possible match. Eg foo.bar.com. would have precedence over bar.com. if rr set name is foo.bar.com. -func findBestMatchingZone( - rrSetName string, - zones []stackitdnsclient.Zone, -) (*stackitdnsclient.Zone, bool) { - count := 0 - var domainZone stackitdnsclient.Zone - for _, zone := range zones { - if len(*zone.DnsName) > count && strings.Contains(rrSetName, *zone.DnsName) { - count = len(*zone.DnsName) - domainZone = zone - } - } - - if count == 0 { - return nil, false - } - - return &domainZone, true -} - -// findRRSet finds a record set by name and type in a list of record sets. -func findRRSet( - rrSetName, recordType string, - rrSets []stackitdnsclient.RecordSet, -) (*stackitdnsclient.RecordSet, bool) { - for _, rrSet := range rrSets { - if *rrSet.Name == rrSetName && *rrSet.Type == recordType { - return &rrSet, true - } - } - - return nil, false -} - -// appendDotIfNotExists appends a dot to the end of a string if it doesn't already end with a dot. -func appendDotIfNotExists(s string) string { - if !strings.HasSuffix(s, ".") { - return s + "." - } - - return s -} - -// modifyChange modifies a change to ensure it is valid for this stackitprovider. -func modifyChange(change *endpoint.Endpoint) { - change.DNSName = appendDotIfNotExists(change.DNSName) - - if change.RecordTTL == 0 { - change.RecordTTL = 300 - } -} - -// getStackitRecordSetPayload returns a stackitdnsclient.RecordSetPayload from a change for the api client. -func getStackitRecordSetPayload(change *endpoint.Endpoint) stackitdnsclient.CreateRecordSetPayload { - records := make([]stackitdnsclient.RecordPayload, len(change.Targets)) - for i := range change.Targets { - records[i] = stackitdnsclient.RecordPayload{ - Content: &change.Targets[i], - } - } - - return stackitdnsclient.CreateRecordSetPayload{ - Name: &change.DNSName, - Records: &records, - Ttl: pointerTo(int64(change.RecordTTL)), - Type: &change.RecordType, - } -} - -// getStackitPartialUpdateRecordSetPayload returns a stackitdnsclient.PartialUpdateRecordSetPayload from a change for the api client. -func getStackitPartialUpdateRecordSetPayload(change *endpoint.Endpoint) stackitdnsclient.PartialUpdateRecordSetPayload { - records := make([]stackitdnsclient.RecordPayload, len(change.Targets)) - for i := range change.Targets { - records[i] = stackitdnsclient.RecordPayload{ - Content: &change.Targets[i], - } - } - - return stackitdnsclient.PartialUpdateRecordSetPayload{ - Name: &change.DNSName, - Records: &records, - Ttl: pointerTo(int64(change.RecordTTL)), - } -} - -// getLogFields returns a log.Fields object for a change. -func getLogFields(change *endpoint.Endpoint, action string, id string) []zap.Field { - return []zap.Field{ - zap.String("record", change.DNSName), - zap.String("content", strings.Join(change.Targets, ",")), - zap.String("type", change.RecordType), - zap.String("action", action), - zap.String("id", id), - } -} - -// pointerTo returns a pointer to the given value. -func pointerTo[T any](v T) *T { - return &v -} diff --git a/internal/stackitprovider/helper_test.go b/internal/stackitprovider/helper_test.go deleted file mode 100644 index 4605cc1..0000000 --- a/internal/stackitprovider/helper_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package stackitprovider - -import ( - "reflect" - "testing" - - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "go.uber.org/zap" - "sigs.k8s.io/external-dns/endpoint" -) - -func TestAppendDotIfNotExists(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - s string - want string - }{ - {"No dot at end", "test", "test."}, - {"Dot at end", "test.", "test."}, - } - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - if got := appendDotIfNotExists(tt.s); got != tt.want { - t.Errorf("appendDotIfNotExists() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestModifyChange(t *testing.T) { - t.Parallel() - - endpointWithTTL := &endpoint.Endpoint{ - DNSName: "test", - RecordTTL: endpoint.TTL(400), - } - modifyChange(endpointWithTTL) - if endpointWithTTL.DNSName != "test." { - t.Errorf("modifyChange() did not append dot to DNSName = %v, want test.", endpointWithTTL.DNSName) - } - if endpointWithTTL.RecordTTL != 400 { - t.Errorf("modifyChange() changed existing RecordTTL = %v, want 400", endpointWithTTL.RecordTTL) - } - - endpointWithoutTTL := &endpoint.Endpoint{ - DNSName: "test", - } - modifyChange(endpointWithoutTTL) - if endpointWithoutTTL.DNSName != "test." { - t.Errorf("modifyChange() did not append dot to DNSName = %v, want test.", endpointWithoutTTL.DNSName) - } - if endpointWithoutTTL.RecordTTL != 300 { - t.Errorf("modifyChange() did not set default RecordTTL = %v, want 300", endpointWithoutTTL.RecordTTL) - } -} - -func TestGetStackitRRSetRecordPost(t *testing.T) { - t.Parallel() - - change := &endpoint.Endpoint{ - DNSName: "test.", - RecordTTL: endpoint.TTL(300), - RecordType: "A", - Targets: endpoint.Targets{ - "192.0.2.1", - "192.0.2.2", - }, - } - expected := stackitdnsclient.CreateRecordSetPayload{ - Name: pointerTo("test."), - Ttl: pointerTo(int64(300)), - Type: pointerTo("A"), - Records: &[]stackitdnsclient.RecordPayload{ - { - Content: pointerTo("192.0.2.1"), - }, - { - Content: pointerTo("192.0.2.2"), - }, - }, - } - got := getStackitRecordSetPayload(change) - if !reflect.DeepEqual(got, expected) { - t.Errorf("getStackitRRSetRecordPost() = %v, want %v", got, expected) - } -} - -func TestFindBestMatchingZone(t *testing.T) { - t.Parallel() - - zones := []stackitdnsclient.Zone{ - {DnsName: pointerTo("foo.com")}, - {DnsName: pointerTo("bar.com")}, - {DnsName: pointerTo("baz.com")}, - } - - tests := []struct { - name string - rrSetName string - want *stackitdnsclient.Zone - wantFound bool - }{ - {"Matching Zone", "www.foo.com", &zones[0], true}, - {"No Matching Zone", "www.test.com", nil, false}, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - got, found := findBestMatchingZone(tt.rrSetName, zones) - if !reflect.DeepEqual(got, tt.want) || found != tt.wantFound { - t.Errorf("findBestMatchingZone() = %v, %v, want %v, %v", got, found, tt.want, tt.wantFound) - } - }) - } -} - -func TestFindRRSet(t *testing.T) { - t.Parallel() - - rrSets := []stackitdnsclient.RecordSet{ - {Name: pointerTo("www.foo.com"), Type: pointerTo("A")}, - {Name: pointerTo("www.bar.com"), Type: pointerTo("A")}, - {Name: pointerTo("www.baz.com"), Type: pointerTo("A")}, - } - - tests := []struct { - name string - rrSetName string - recordType string - want *stackitdnsclient.RecordSet - wantFound bool - }{ - {"Matching RRSet", "www.foo.com", "A", &rrSets[0], true}, - {"No Matching RRSet", "www.test.com", "A", nil, false}, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got, found := findRRSet(tt.rrSetName, tt.recordType, rrSets) - if !reflect.DeepEqual(got, tt.want) || found != tt.wantFound { - t.Errorf("findRRSet() = %v, %v, want %v, %v", got, found, tt.want, tt.wantFound) - } - }) - } -} - -func TestGetLogFields(t *testing.T) { - t.Parallel() - - change := &endpoint.Endpoint{ - DNSName: "test.", - RecordTTL: endpoint.TTL(300), - RecordType: "A", - Targets: endpoint.Targets{ - "192.0.2.1", - "192.0.2.2", - }, - } - - expected := []zap.Field{ - zap.String("record", "test."), - zap.String("content", "192.0.2.1,192.0.2.2"), - zap.String("type", "A"), - zap.String("action", "create"), - zap.String("id", "123"), - } - - got := getLogFields(change, "create", "123") - - if !reflect.DeepEqual(got, expected) { - t.Errorf("getLogFields() = %v, want %v", got, expected) - } -} - -func TestGetStackitRRSetRecordPatch(t *testing.T) { - t.Parallel() - - change := &endpoint.Endpoint{ - DNSName: "test.", - RecordTTL: endpoint.TTL(300), - RecordType: "A", - Targets: endpoint.Targets{ - "192.0.2.1", - "192.0.2.2", - }, - } - - expected := stackitdnsclient.PartialUpdateRecordSetPayload{ - Name: pointerTo("test."), - Ttl: pointerTo(int64(300)), - Records: &[]stackitdnsclient.RecordPayload{ - { - Content: pointerTo("192.0.2.1"), - }, - { - Content: pointerTo("192.0.2.2"), - }, - }, - } - - got := getStackitPartialUpdateRecordSetPayload(change) - - if !reflect.DeepEqual(got, expected) { - t.Errorf("getStackitRRSetRecordPatch() = %v, want %v", got, expected) - } -} diff --git a/internal/stackitprovider/models.go b/internal/stackitprovider/models.go deleted file mode 100644 index 07fe33c..0000000 --- a/internal/stackitprovider/models.go +++ /dev/null @@ -1,26 +0,0 @@ -package stackitprovider - -import "sigs.k8s.io/external-dns/endpoint" - -const ( - CREATE = "CREATE" - UPDATE = "UPDATE" - DELETE = "DELETE" -) - -// ErrorMessage is the error message returned by the API. -type ErrorMessage struct { - Message string `json:"message"` -} - -// changeTask is a task that is passed to the worker. -type changeTask struct { - change *endpoint.Endpoint - action string -} - -// endpointError is a list of endpoints and an error to pass to workers. -type endpointError struct { - endpoints []*endpoint.Endpoint - err error -} diff --git a/internal/stackitprovider/records.go b/internal/stackitprovider/records.go deleted file mode 100644 index 802a140..0000000 --- a/internal/stackitprovider/records.go +++ /dev/null @@ -1,104 +0,0 @@ -package stackitprovider - -import ( - "context" - - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/provider" -) - -// Records returns resource records. -func (d *StackitDNSProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - zones, err := d.zoneFetcherClient.zones(ctx) - if err != nil { - return nil, err - } - - var endpoints []*endpoint.Endpoint - endpointsErrorChannel := make(chan endpointError, len(zones)) - zoneIdsChannel := make(chan string, len(zones)) - - for i := 0; i < d.workers; i++ { - go d.fetchRecordsWorker(ctx, zoneIdsChannel, endpointsErrorChannel) - } - - for _, zone := range zones { - zoneIdsChannel <- *zone.Id - } - - for i := 0; i < len(zones); i++ { - endpointsErrorList := <-endpointsErrorChannel - if endpointsErrorList.err != nil { - close(zoneIdsChannel) - - return nil, endpointsErrorList.err - } - endpoints = append(endpoints, endpointsErrorList.endpoints...) - } - - close(zoneIdsChannel) - - return endpoints, nil -} - -// fetchRecordsWorker fetches all records from a given zone. -func (d *StackitDNSProvider) fetchRecordsWorker( - ctx context.Context, - zoneIdChannel chan string, - endpointsErrorChannel chan<- endpointError, -) { - for zoneId := range zoneIdChannel { - d.processZoneRRSets(ctx, zoneId, endpointsErrorChannel) - } - - d.logger.Debug("fetch record set worker finished") -} - -// processZoneRRSets fetches and processes DNS records for a given zone. -func (d *StackitDNSProvider) processZoneRRSets( - ctx context.Context, - zoneId string, - endpointsErrorChannel chan<- endpointError, -) { - var endpoints []*endpoint.Endpoint - rrSets, err := d.rrSetFetcherClient.fetchRecords(ctx, zoneId, nil) - if err != nil { - endpointsErrorChannel <- endpointError{ - endpoints: nil, - err: err, - } - - return - } - - endpoints = d.collectEndPoints(rrSets) - endpointsErrorChannel <- endpointError{ - endpoints: endpoints, - err: nil, - } -} - -// collectEndPoints creates a list of Endpoints from the provided rrSets. -func (d *StackitDNSProvider) collectEndPoints( - rrSets []stackitdnsclient.RecordSet, -) []*endpoint.Endpoint { - var endpoints []*endpoint.Endpoint - for _, r := range rrSets { - if provider.SupportedRecordType(*r.Type) { - for _, _r := range *r.Records { - endpoints = append( - endpoints, - endpoint.NewEndpointWithTTL( - *r.Name, - *r.Type, - endpoint.TTL(*r.Ttl), - *_r.Content, - ), - ) - } - } - } - - return endpoints -} diff --git a/internal/stackitprovider/records_test.go b/internal/stackitprovider/records_test.go deleted file mode 100644 index f099081..0000000 --- a/internal/stackitprovider/records_test.go +++ /dev/null @@ -1,395 +0,0 @@ -package stackitprovider - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/goccy/go-json" - stackitconfig "github.com/stackitcloud/stackit-sdk-go/core/config" - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "github.com/stretchr/testify/assert" - "go.uber.org/zap" - "sigs.k8s.io/external-dns/endpoint" -) - -func TestRecords(t *testing.T) { - t.Parallel() - - server := getServerRecords(t) - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - endpoints, err := stackitDnsProvider.Records(context.Background()) - assert.NoError(t, err) - assert.Equal(t, 2, len(endpoints)) - assert.Equal(t, "test.com", endpoints[0].DNSName) - assert.Equal(t, "A", endpoints[0].RecordType) - assert.Equal(t, "1.2.3.4", endpoints[0].Targets[0]) - assert.Equal(t, int64(300), int64(endpoints[0].RecordTTL)) - - assert.Equal(t, "test2.com", endpoints[1].DNSName) - assert.Equal(t, "A", endpoints[1].RecordType) - assert.Equal(t, "5.6.7.8", endpoints[1].Targets[0]) - assert.Equal(t, int64(300), int64(endpoints[1].RecordTTL)) -} - -// TestWrongJsonResponseRecords tests the scenario where the server returns a wrong JSON response. -func TestWrongJsonResponseRecords(t *testing.T) { - t.Parallel() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - - mux.HandleFunc("/v1/projects/1234/zones", - func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - w.WriteHeader(http.StatusOK) - w.Write([]byte(`{"invalid:"json"`)) // This is not a valid JSON. - }, - ) - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - endpoints, err := stackitDnsProvider.Records(context.Background()) - assert.Error(t, err) - assert.Equal(t, 0, len(endpoints)) -} - -// TestPagedResponseRecords tests the scenario where the server returns a paged response. -func TestPagedResponseRecords(t *testing.T) { - t.Parallel() - - server := getPagedRecordsServer(t) - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - endpoints, err := stackitDnsProvider.Records(context.Background()) - assert.NoError(t, err) - assert.Equal(t, 2, len(endpoints)) - assert.Equal(t, "test.com", endpoints[0].DNSName) - assert.Equal(t, "A", endpoints[0].RecordType) - assert.Equal(t, "1.2.3.4", endpoints[0].Targets[0]) - assert.Equal(t, int64(300), int64(endpoints[0].RecordTTL)) - - assert.Equal(t, "test2.com", endpoints[1].DNSName) - assert.Equal(t, "A", endpoints[1].RecordType) - assert.Equal(t, "5.6.7.8", endpoints[1].Targets[0]) - assert.Equal(t, int64(300), int64(endpoints[1].RecordTTL)) -} - -func TestEmptyZonesRouteRecords(t *testing.T) { - t.Parallel() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - _, err = stackitDnsProvider.Records(context.Background()) - assert.Error(t, err) -} - -func TestEmptyRRSetRouteRecords(t *testing.T) { - t.Parallel() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - mux.HandleFunc("/v1/projects/1234/zones", - func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - zones := stackitdnsclient.ListZonesResponse{ - ItemsPerPage: pointerTo(int64(1)), - Message: pointerTo("success"), - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(2)), - Zones: &[]stackitdnsclient.Zone{{Id: pointerTo("1234")}}, - } - successResponseBytes, err := json.Marshal(zones) - assert.NoError(t, err) - - w.WriteHeader(http.StatusOK) - w.Write(successResponseBytes) - }, - ) - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - _, err = stackitDnsProvider.Records(context.Background()) - assert.Error(t, err) -} - -func TestZoneEndpoint500Records(t *testing.T) { - t.Parallel() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - mux.HandleFunc("/v1/projects/1234/zones", - func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - w.WriteHeader(http.StatusInternalServerError) - }, - ) - defer server.Close() - - stackitDnsProvider, err := getDefaultTestProvider(server) - assert.NoError(t, err) - - _, err = stackitDnsProvider.Records(context.Background()) - assert.Error(t, err) -} - -func TestZoneEndpoint403Records(t *testing.T) { - t.Parallel() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - mux.HandleFunc("/v1/projects/1234/zones", - func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - w.WriteHeader(http.StatusForbidden) - }, - ) - defer server.Close() - - stackitDnsProvider, err := NewStackitDNSProvider( - zap.NewNop(), - Config{ - ProjectId: "1234", - DomainFilter: endpoint.DomainFilter{}, - DryRun: false, - Workers: 10, - }, - stackitconfig.WithHTTPClient(server.Client()), - stackitconfig.WithEndpoint(server.URL), - // we need a non-empty token for the bootstrapping not to fail - stackitconfig.WithToken("token"), - ) - assert.NoError(t, err) - - _, err = stackitDnsProvider.Records(context.Background()) - assert.Error(t, err) -} - -func getDefaultTestProvider(server *httptest.Server) (*StackitDNSProvider, error) { - stackitDnsProvider, err := NewStackitDNSProvider( - zap.NewNop(), - Config{ - ProjectId: "1234", - DomainFilter: endpoint.DomainFilter{}, - DryRun: false, - Workers: 1, - }, - stackitconfig.WithHTTPClient(server.Client()), - stackitconfig.WithEndpoint(server.URL), - // we need a non-empty token for the bootstrapping not to fail - stackitconfig.WithToken("token")) - - return stackitDnsProvider, err -} - -func getZonesHandlerRecordsPaged(t *testing.T) http.HandlerFunc { - t.Helper() - - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - zones := stackitdnsclient.ListZonesResponse{} - if r.URL.Query().Get("page") == "1" { - zones = stackitdnsclient.ListZonesResponse{ - ItemsPerPage: pointerTo(int64(1)), - Message: pointerTo("success"), - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(2)), - Zones: &[]stackitdnsclient.Zone{{Id: pointerTo("1234")}}, - } - } - if r.URL.Query().Get("page") == "2" { - zones = stackitdnsclient.ListZonesResponse{ - ItemsPerPage: pointerTo(int64(1)), - Message: pointerTo("success"), - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(2)), - Zones: &[]stackitdnsclient.Zone{{Id: pointerTo("5678")}}, - } - } - successResponseBytes, err := json.Marshal(zones) - assert.NoError(t, err) - - w.WriteHeader(http.StatusOK) - w.Write(successResponseBytes) - } -} - -func getRrsetsHandlerReecodsPaged(t *testing.T, domain string) http.HandlerFunc { - t.Helper() - - return func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - rrSets := stackitdnsclient.ListRecordSetsResponse{} - if domain == "1234" { - rrSets = stackitdnsclient.ListRecordSetsResponse{ - ItemsPerPage: pointerTo(int64(1)), - Message: pointerTo("success"), - RrSets: &[]stackitdnsclient.RecordSet{ - { - Name: pointerTo("test.com."), - Type: pointerTo("A"), - Ttl: pointerTo(int64(300)), - Records: &[]stackitdnsclient.Record{ - {Content: pointerTo("1.2.3.4")}, - }, - }, - }, - TotalItems: pointerTo(int64(1)), - TotalPages: pointerTo(int64(1)), - } - } - if domain == "5678" { - rrSets = stackitdnsclient.ListRecordSetsResponse{ - ItemsPerPage: pointerTo(int64(1)), - Message: pointerTo("success"), - RrSets: &[]stackitdnsclient.RecordSet{ - { - Name: pointerTo("test2.com."), - Type: pointerTo("A"), - Ttl: pointerTo(int64(300)), - Records: &[]stackitdnsclient.Record{ - {Content: pointerTo("5.6.7.8")}, - }, - }, - }, - TotalItems: pointerTo(int64(1)), - TotalPages: pointerTo(int64(1)), - } - } - - successResponseBytes, err := json.Marshal(rrSets) - assert.NoError(t, err) - - w.WriteHeader(http.StatusOK) - w.Write(successResponseBytes) - } -} - -func getPagedRecordsServer(t *testing.T) *httptest.Server { - t.Helper() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - - mux.HandleFunc("/v1/projects/1234/zones", getZonesHandlerRecordsPaged(t)) - mux.HandleFunc("/v1/projects/1234/zones/1234/rrsets", getRrsetsHandlerReecodsPaged(t, "1234")) - mux.HandleFunc("/v1/projects/1234/zones/5678/rrsets", getRrsetsHandlerReecodsPaged(t, "5678")) - - return server -} - -func getZonesResponseRecordsNonPaged(t *testing.T, w http.ResponseWriter) { - t.Helper() - - w.Header().Set("Content-Type", "application/json") - - zones := stackitdnsclient.ListZonesResponse{ - ItemsPerPage: pointerTo(int64(10)), - Message: pointerTo("success"), - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(1)), - Zones: &[]stackitdnsclient.Zone{ - {Id: pointerTo("1234"), DnsName: pointerTo("test.com")}, - {Id: pointerTo("5678"), DnsName: pointerTo("test2.com")}, - }, - } - successResponseBytes, err := json.Marshal(zones) - assert.NoError(t, err) - - w.WriteHeader(http.StatusOK) - w.Write(successResponseBytes) -} - -func getRrsetsResponseRecordsNonPaged(t *testing.T, w http.ResponseWriter, domain string) { - t.Helper() - - w.Header().Set("Content-Type", "application/json") - - var rrSets stackitdnsclient.ListRecordSetsResponse - - if domain == "1234" { - rrSets = stackitdnsclient.ListRecordSetsResponse{ - ItemsPerPage: pointerTo(int64(20)), - Message: pointerTo("success"), - RrSets: &[]stackitdnsclient.RecordSet{ - { - Name: pointerTo("test.com."), - Type: pointerTo("A"), - Ttl: pointerTo(int64(300)), - Records: &[]stackitdnsclient.Record{ - {Content: pointerTo("1.2.3.4")}, - }, - Id: pointerTo("1234"), - }, - }, - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(1)), - } - } else if domain == "5678" { - rrSets = stackitdnsclient.ListRecordSetsResponse{ - ItemsPerPage: pointerTo(int64(20)), - Message: pointerTo("success"), - RrSets: &[]stackitdnsclient.RecordSet{ - { - Name: pointerTo("test2.com."), - Type: pointerTo("A"), - Ttl: pointerTo(int64(300)), - Records: &[]stackitdnsclient.Record{ - {Content: pointerTo("5.6.7.8")}, - }, - Id: pointerTo("5678"), - }, - }, - TotalItems: pointerTo(int64(2)), - TotalPages: pointerTo(int64(1)), - } - } - - successResponseBytes, err := json.Marshal(rrSets) - assert.NoError(t, err) - - w.WriteHeader(http.StatusOK) - w.Write(successResponseBytes) -} - -func getServerRecords(t *testing.T) *httptest.Server { - t.Helper() - - mux := http.NewServeMux() - server := httptest.NewServer(mux) - - mux.HandleFunc("/v1/projects/1234/zones", func(w http.ResponseWriter, r *http.Request) { - getZonesResponseRecordsNonPaged(t, w) - }) - mux.HandleFunc("/v1/projects/1234/zones/1234/rrsets", func(w http.ResponseWriter, r *http.Request) { - getRrsetsResponseRecordsNonPaged(t, w, "1234") - }) - mux.HandleFunc("/v1/projects/1234/zones/5678/rrsets", func(w http.ResponseWriter, r *http.Request) { - getRrsetsResponseRecordsNonPaged(t, w, "5678") - }) - - return server -} diff --git a/internal/stackitprovider/rrset_fetcher.go b/internal/stackitprovider/rrset_fetcher.go deleted file mode 100644 index 4819df1..0000000 --- a/internal/stackitprovider/rrset_fetcher.go +++ /dev/null @@ -1,99 +0,0 @@ -package stackitprovider - -import ( - "context" - "fmt" - - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "go.uber.org/zap" - "sigs.k8s.io/external-dns/endpoint" -) - -type rrSetFetcher struct { - apiClient *stackitdnsclient.APIClient - domainFilter endpoint.DomainFilter - projectId string - logger *zap.Logger -} - -func newRRSetFetcher( - apiClient *stackitdnsclient.APIClient, - domainFilter endpoint.DomainFilter, - projectId string, - logger *zap.Logger, -) *rrSetFetcher { - return &rrSetFetcher{ - apiClient: apiClient, - domainFilter: domainFilter, - projectId: projectId, - logger: logger, - } -} - -// fetchRecords fetches all []stackitdnsclient.RecordSet from STACKIT DNS API for given zone id. -func (r *rrSetFetcher) fetchRecords( - ctx context.Context, - zoneId string, - nameFilter *string, -) ([]stackitdnsclient.RecordSet, error) { - var result []stackitdnsclient.RecordSet - var pager int32 = 1 - - listRequest := r.apiClient.ListRecordSets(ctx, r.projectId, zoneId).Page(pager).PageSize(10000).ActiveEq(true) - - if nameFilter != nil { - listRequest = listRequest.NameLike(*nameFilter) - } - - rrSetResponse, err := listRequest.Execute() - if err != nil { - return nil, err - } - - result = append(result, *rrSetResponse.RrSets...) - - // if there is more than one page, we need to loop over the other pages and - // issue another API request for each one of them - pager++ - for int64(pager) <= *rrSetResponse.TotalPages { - rrSetResponse, err := listRequest.Page(pager).Execute() - if err != nil { - return nil, err - } - result = append(result, *rrSetResponse.RrSets...) - pager++ - } - - return result, nil -} - -// getRRSetForUpdateDeletion returns the record set to be deleted and the zone it belongs to. -func (r *rrSetFetcher) getRRSetForUpdateDeletion( - ctx context.Context, - change *endpoint.Endpoint, - zones []stackitdnsclient.Zone, -) (*stackitdnsclient.Zone, *stackitdnsclient.RecordSet, error) { - resultZone, found := findBestMatchingZone(change.DNSName, zones) - if !found { - r.logger.Error( - "record set name contains no zone dns name", - zap.String("name", change.DNSName), - ) - - return nil, nil, fmt.Errorf("record set name contains no zone dns name") - } - - domainRRSets, err := r.fetchRecords(ctx, *resultZone.Id, &change.DNSName) - if err != nil { - return nil, nil, err - } - - resultRRSet, found := findRRSet(change.DNSName, change.RecordType, domainRRSets) - if !found { - r.logger.Info("record not found on record sets", zap.String("name", change.DNSName)) - - return nil, nil, fmt.Errorf("record not found on record sets") - } - - return resultZone, resultRRSet, nil -} diff --git a/internal/stackitprovider/stackit.go b/internal/stackitprovider/stackit.go deleted file mode 100644 index 8cb584d..0000000 --- a/internal/stackitprovider/stackit.go +++ /dev/null @@ -1,47 +0,0 @@ -package stackitprovider - -import ( - stackitconfig "github.com/stackitcloud/stackit-sdk-go/core/config" - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "go.uber.org/zap" - "sigs.k8s.io/external-dns/endpoint" - "sigs.k8s.io/external-dns/provider" -) - -// StackitDNSProvider implements the DNS stackitprovider for STACKIT DNS. -type StackitDNSProvider struct { - provider.BaseProvider - projectId string - domainFilter endpoint.DomainFilter - dryRun bool - workers int - logger *zap.Logger - apiClient *stackitdnsclient.APIClient - zoneFetcherClient *zoneFetcher - rrSetFetcherClient *rrSetFetcher -} - -// NewStackitDNSProvider creates a new STACKIT DNS stackitprovider. -func NewStackitDNSProvider( - logger *zap.Logger, - providerConfig Config, - stackitConfig ...stackitconfig.ConfigurationOption, -) (*StackitDNSProvider, error) { - apiClient, err := stackitdnsclient.NewAPIClient(stackitConfig...) - if err != nil { - return nil, err - } - - provider := &StackitDNSProvider{ - apiClient: apiClient, - domainFilter: providerConfig.DomainFilter, - dryRun: providerConfig.DryRun, - projectId: providerConfig.ProjectId, - workers: providerConfig.Workers, - logger: logger, - zoneFetcherClient: newZoneFetcher(apiClient, providerConfig.DomainFilter, providerConfig.ProjectId), - rrSetFetcherClient: newRRSetFetcher(apiClient, providerConfig.DomainFilter, providerConfig.ProjectId, logger), - } - - return provider, nil -} diff --git a/internal/stackitprovider/stackit_test.go b/internal/stackitprovider/stackit_test.go deleted file mode 100644 index fdbf777..0000000 --- a/internal/stackitprovider/stackit_test.go +++ /dev/null @@ -1 +0,0 @@ -package stackitprovider diff --git a/internal/stackitprovider/zone_fetcher.go b/internal/stackitprovider/zone_fetcher.go deleted file mode 100644 index b2e6c57..0000000 --- a/internal/stackitprovider/zone_fetcher.go +++ /dev/null @@ -1,84 +0,0 @@ -package stackitprovider - -import ( - "context" - - stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" - "sigs.k8s.io/external-dns/endpoint" -) - -type zoneFetcher struct { - apiClient *stackitdnsclient.APIClient - domainFilter endpoint.DomainFilter - projectId string -} - -func newZoneFetcher( - apiClient *stackitdnsclient.APIClient, - domainFilter endpoint.DomainFilter, - projectId string, -) *zoneFetcher { - return &zoneFetcher{ - apiClient: apiClient, - domainFilter: domainFilter, - projectId: projectId, - } -} - -// zones returns filtered list of stackitdnsclient.Zone if filter is set. -func (z *zoneFetcher) zones(ctx context.Context) ([]stackitdnsclient.Zone, error) { - if len(z.domainFilter.Filters) == 0 { - // no filters, return all zones - listRequest := z.apiClient.ListZones(ctx, z.projectId).ActiveEq(true) - zones, err := z.fetchZones(listRequest) - if err != nil { - return nil, err - } - - return zones, nil - } - - var result []stackitdnsclient.Zone - // send one request per filter - for _, filter := range z.domainFilter.Filters { - listRequest := z.apiClient.ListZones(ctx, z.projectId).ActiveEq(true).DnsNameLike(filter) - zones, err := z.fetchZones(listRequest) - if err != nil { - return nil, err - } - result = append(result, zones...) - } - - return result, nil -} - -// fetchZones fetches all []stackitdnsclient.Zone from STACKIT DNS API. -func (z *zoneFetcher) fetchZones( - listRequest stackitdnsclient.ApiListZonesRequest, -) ([]stackitdnsclient.Zone, error) { - var result []stackitdnsclient.Zone - var pager int32 = 1 - - listRequest = listRequest.Page(1).PageSize(10000) - - zoneResponse, err := listRequest.Execute() - if err != nil { - return nil, err - } - - result = append(result, *zoneResponse.Zones...) - - // if there is more than one page, we need to loop over the other pages and - // issue another API request for each one of them - pager++ - for int64(pager) <= *zoneResponse.TotalPages { - zoneResponse, err := listRequest.Page(pager).Execute() - if err != nil { - return nil, err - } - result = append(result, *zoneResponse.Zones...) - pager++ - } - - return result, nil -} From f70d607a892fc543442a227e2ef1eac3e3a7e993 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 13:31:37 +0300 Subject: [PATCH 05/19] fix: dockerfile & removed redundant stackit package --- Dockerfile | 4 ++-- pkg/stackit/options.go | 40 ------------------------------- pkg/stackit/options_test.go | 47 ------------------------------------- 3 files changed, 2 insertions(+), 89 deletions(-) delete mode 100644 pkg/stackit/options.go delete mode 100644 pkg/stackit/options_test.go diff --git a/Dockerfile b/Dockerfile index 2be6e95..d2d8ab0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM gcr.io/distroless/static-debian11:nonroot -COPY external-dns-webhook /external-dns-webhook +COPY external-dns-stackit-webhook /external-dns-stackit-webhook -ENTRYPOINT ["/external-dns-webhook"] +ENTRYPOINT ["/external-dns-stackit-webhook"] \ No newline at end of file diff --git a/pkg/stackit/options.go b/pkg/stackit/options.go deleted file mode 100644 index 69fc2b0..0000000 --- a/pkg/stackit/options.go +++ /dev/null @@ -1,40 +0,0 @@ -package stackit - -import ( - "fmt" - "net/http" - "time" - - stackitconfig "github.com/stackitcloud/stackit-sdk-go/core/config" -) - -// SetConfigOptions sets the default config options for the STACKIT -// client and determines which type of authorization to use, depending on the -// passed bearerToken and keyPath parameters. If no baseURL or an invalid -// combination of auth options is given (neither or both), the function returns -// an error. -func SetConfigOptions(baseURL, bearerToken, keyPath string) ([]stackitconfig.ConfigurationOption, error) { - if len(baseURL) == 0 { - return nil, fmt.Errorf("base-url is required") - } - - options := []stackitconfig.ConfigurationOption{ - stackitconfig.WithHTTPClient(&http.Client{ - Timeout: 10 * time.Second, - }), - stackitconfig.WithEndpoint(baseURL), - } - - bearerTokenSet := len(bearerToken) > 0 - keyPathSet := len(keyPath) > 0 - - if (!bearerTokenSet && !keyPathSet) || (bearerTokenSet && keyPathSet) { - return nil, fmt.Errorf("exactly only one of auth-token or auth-key-path is required") - } - - if bearerTokenSet { - return append(options, stackitconfig.WithToken(bearerToken)), nil - } - - return append(options, stackitconfig.WithServiceAccountKeyPath(keyPath)), nil -} diff --git a/pkg/stackit/options_test.go b/pkg/stackit/options_test.go deleted file mode 100644 index 9a024c0..0000000 --- a/pkg/stackit/options_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package stackit - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestMissingBaseURL(t *testing.T) { - t.Parallel() - - options, err := SetConfigOptions("", "", "") - assert.ErrorContains(t, err, "base-url") - assert.Nil(t, options) -} - -func TestBothAuthOptionsMissing(t *testing.T) { - t.Parallel() - - options, err := SetConfigOptions("https://example.com", "", "") - assert.ErrorContains(t, err, "auth-token or auth-key-path") - assert.Nil(t, options) -} - -func TestBothAuthOptionsSet(t *testing.T) { - t.Parallel() - - options, err := SetConfigOptions("https://example.com", "token", "key/path") - assert.ErrorContains(t, err, "auth-token or auth-key-path") - assert.Nil(t, options) -} - -func TestBearerTokenSet(t *testing.T) { - t.Parallel() - - options, err := SetConfigOptions("https://example.com", "token", "") - assert.NoError(t, err) - assert.Len(t, options, 3) -} - -func TestKeyPathSet(t *testing.T) { - t.Parallel() - - options, err := SetConfigOptions("https://example.com", "", "key/path") - assert.NoError(t, err) - assert.Len(t, options, 3) -} From f8263420ba568c03953091f9e72ef79f7cccf43b Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 14:02:44 +0300 Subject: [PATCH 06/19] refactor: added blank lines at the end of files --- Dockerfile | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d2d8ab0..602ced9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,4 +2,4 @@ FROM gcr.io/distroless/static-debian11:nonroot COPY external-dns-stackit-webhook /external-dns-stackit-webhook -ENTRYPOINT ["/external-dns-stackit-webhook"] \ No newline at end of file +ENTRYPOINT ["/external-dns-stackit-webhook"] diff --git a/Makefile b/Makefile index 5142615..72b1a77 100644 --- a/Makefile +++ b/Makefile @@ -79,4 +79,4 @@ license-check: $(GO_LICENSES) reports ## Check licenses against code. .PHONY: license-report license-report: $(GO_LICENSES) reports ## Create licenses report against code. - $(GO_LICENSES) report --include_tests --ignore $(LICENCES_IGNORE_LIST) ./... > ./reports/licenses/licenses-list.csv \ No newline at end of file + $(GO_LICENSES) report --include_tests --ignore $(LICENCES_IGNORE_LIST) ./... > ./reports/licenses/licenses-list.csv From 33464af5e4b96f5581ca014f225fbc0c68294279 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 14:10:15 +0300 Subject: [PATCH 07/19] fix: removed faq because it contains non-relevant answers for selectel --- README.md | 95 ------------------------------------------------------- 1 file changed, 95 deletions(-) diff --git a/README.md b/README.md index c77901d..30b8b1c 100644 --- a/README.md +++ b/README.md @@ -232,101 +232,6 @@ Below are the options that are available. - `--log-level`/`LOG_LEVEL` (optional): Defines the log level (default "info"). Possible values are: debug, info, warn, error. -## FAQ - -### 1. Issue with Creating Service using External DNS Annotation - -If your zone is `example.runs.selcloud.ru` and you're trying to create a service with the following external DNS -annotation: - - ```yaml - apiVersion: v1 - kind: Service - metadata: - annotations: - external-dns.alpha.kubernetes.io/hostname: example.runs.selcloud.ru - labels: - app.kubernetes.io/name: ingress-nginx - app.kubernetes.io/instance: nginx - app.kubernetes.io/part-of: ingress-nginx - app.kubernetes.io/component: controller - name: nginx-ingress-controller - namespace: nginx-ingress-controller - spec: - type: LoadBalancer - externalTrafficPolicy: Local - ipFamilyPolicy: SingleStack - ipFamilies: - - IPv4 - ports: - - name: http - port: 80 - protocol: TCP - targetPort: http - - name: https - port: 443 - protocol: TCP - targetPort: https - selector: - app.kubernetes.io/component: controller - app.kubernetes.io/instance: nginx - app.kubernetes.io/name: ingress-nginx - ``` - -Why isn't it working? - -Answer: The External DNS will try to create a TXT record named `a-example.runs.selcloud.ru`, which will fail -because you can't establish a record outside the zone. The solution is to use a name that's within the zone, such as -`nginx.example.runs.selcloud.ru`. - -### 2. Issues with Creating Ingresses not in the Zone - -For a project containing the zone `example.runs.selcloud.ru`, suppose you've created these two ingress: - - ```yaml - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - annotations: - ingress.kubernetes.io/rewrite-target: / - kubernetes.io/ingress.class: nginx - name: example-ingress-external-dns - namespace: default - spec: - rules: - - host: test.example.runs.selcloud.ru - http: - paths: - - backend: - service: - name: example - port: - number: 80 - path: / - pathType: Prefix - - host: test.example.stackit.rocks - http: - paths: - - backend: - service: - name: example - port: - number: 80 - path: / - pathType: Prefix - ``` - -Why isn't it working? - -Answer: External DNS will attempt to establish a record set for `test.example.stackit.rocks`. As the zone -`example.stackit.rocks` isn't within the project, it'll fail. There are two potential fixes: - -- Incorporate the zone `example.stackit.rocks` into the project. -- Adjust the domain filter to `example.runs.selcloud.ru` by setting the domain filter - flag `--domain-filter="example.runs.selcloud.ru"`. This will exclude `test.example.stackit.rocks` and only - generate - the record set for `test.example.runs.selcloud.ru`. - ## Development Run the app: From e501350f8a7d77190df814ef10a528d78842499e Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 15:48:21 +0300 Subject: [PATCH 08/19] refactor & docs for selprovider, keystone and httpdefault --- internal/selprovider/apply_changes.go | 139 ++++++++++++-------------- internal/selprovider/config.go | 13 ++- internal/selprovider/provider.go | 11 +- pkg/httpclient/default.go | 54 ---------- pkg/httpdefault/default.go | 62 ++++++++++++ pkg/keystone/provider.go | 19 ++-- 6 files changed, 149 insertions(+), 149 deletions(-) delete mode 100644 pkg/httpclient/default.go create mode 100644 pkg/httpdefault/default.go diff --git a/internal/selprovider/apply_changes.go b/internal/selprovider/apply_changes.go index b7829d0..af7173f 100644 --- a/internal/selprovider/apply_changes.go +++ b/internal/selprovider/apply_changes.go @@ -10,7 +10,7 @@ import ( "sigs.k8s.io/external-dns/plan" ) -// ApplyChanges applies a given set of changes in a given zone. +// ApplyChanges applies a given set of changes. func (p *Provider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { client, err := p.getDomainsClient() if err != nil { @@ -48,12 +48,74 @@ func (p *Provider) createRRSets( return nil } + return p.handleRRSetWithWorkers(ctx, client, endpoints, CREATE) +} + +// updateRRSets patches (overrides) contents in the record sets for the given endpoints that are in the update new field. +func (p *Provider) updateRRSets( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + endpoints []*endpoint.Endpoint, +) error { + if len(endpoints) == 0 { + return nil + } + + return p.handleRRSetWithWorkers(ctx, client, endpoints, UPDATE) +} + +// deleteRRSets delete record sets for the given endpoints that are in the deletion field. +func (p *Provider) deleteRRSets( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + endpoints []*endpoint.Endpoint, +) error { + if len(endpoints) == 0 { + p.logger.Debug("no endpoints to delete") + + return nil + } + + p.logger.Info("records to delete", zap.String("records", fmt.Sprintf("%v", endpoints))) + + return p.handleRRSetWithWorkers(ctx, client, endpoints, DELETE) +} + +// handleRRSetWithWorkers handles the given endpoints with workers to optimize speed. +func (p *Provider) handleRRSetWithWorkers( + ctx context.Context, + client domains.DNSClient[domains.Zone, domains.RRSet], + endpoints []*endpoint.Endpoint, + action string, +) error { zones, err := p.zoneFetcherClient.zones(ctx, client) if err != nil { return err } - return p.handleRRSetWithWorkers(ctx, client, endpoints, zones, CREATE) + workerChannel := make(chan changeTask, len(endpoints)) + defer close(workerChannel) + errorChannel := make(chan error, len(endpoints)) + + for i := 0; i < p.workers; i++ { + go p.changeWorker(ctx, client, workerChannel, errorChannel, zones) + } + + for _, change := range endpoints { + workerChannel <- changeTask{ + action: action, + change: change, + } + } + + for i := 0; i < len(endpoints); i++ { + err := <-errorChannel + if err != nil { + return err + } + } + + return nil } // createRRSet creates a new record set for the given endpoint. @@ -94,24 +156,6 @@ func (p *Provider) createRRSet( return nil } -// updateRRSets patches (overrides) contents in the record sets for the given endpoints that are in the update new field. -func (p *Provider) updateRRSets( - ctx context.Context, - client domains.DNSClient[domains.Zone, domains.RRSet], - endpoints []*endpoint.Endpoint, -) error { - if len(endpoints) == 0 { - return nil - } - - zones, err := p.zoneFetcherClient.zones(ctx, client) - if err != nil { - return err - } - - return p.handleRRSetWithWorkers(ctx, client, endpoints, zones, UPDATE) -} - // updateRRSet patches (overrides) contents in the record set. func (p *Provider) updateRRSet( ctx context.Context, @@ -149,28 +193,6 @@ func (p *Provider) updateRRSet( return nil } -// deleteRRSets delete record sets for the given endpoints that are in the deletion field. -func (p *Provider) deleteRRSets( - ctx context.Context, - client domains.DNSClient[domains.Zone, domains.RRSet], - endpoints []*endpoint.Endpoint, -) error { - if len(endpoints) == 0 { - p.logger.Debug("no endpoints to delete") - - return nil - } - - p.logger.Info("records to delete", zap.String("records", fmt.Sprintf("%v", endpoints))) - - zones, err := p.zoneFetcherClient.zones(ctx, client) - if err != nil { - return err - } - - return p.handleRRSetWithWorkers(ctx, client, endpoints, zones, DELETE) -} - // deleteRRSet deletes a record set for the given endpoint. func (p *Provider) deleteRRSet( ctx context.Context, @@ -206,39 +228,6 @@ func (p *Provider) deleteRRSet( return nil } -// handleRRSetWithWorkers handles the given endpoints with workers to optimize speed. -func (p *Provider) handleRRSetWithWorkers( - ctx context.Context, - client domains.DNSClient[domains.Zone, domains.RRSet], - endpoints []*endpoint.Endpoint, - zones []*domains.Zone, - action string, -) error { - workerChannel := make(chan changeTask, len(endpoints)) - defer close(workerChannel) - errorChannel := make(chan error, len(endpoints)) - - for i := 0; i < p.workers; i++ { - go p.changeWorker(ctx, client, workerChannel, errorChannel, zones) - } - - for _, change := range endpoints { - workerChannel <- changeTask{ - action: action, - change: change, - } - } - - for i := 0; i < len(endpoints); i++ { - err := <-errorChannel - if err != nil { - return err - } - } - - return nil -} - // changeWorker is a worker that handles changes passed by a channel. func (p *Provider) changeWorker( ctx context.Context, diff --git a/internal/selprovider/config.go b/internal/selprovider/config.go index 9c668a9..2027557 100644 --- a/internal/selprovider/config.go +++ b/internal/selprovider/config.go @@ -6,11 +6,16 @@ import ( // Config is used to configure the creation of the Provider. type Config struct { - BaseURL string + // BaseURL is a Selectel DNS API endpoint for v2.DNSClient + BaseURL string + // KeystoneProvider needed to generate X-Auth-Token header with keystone-header for requests to the DNS API. KeystoneProvider KeystoneProvider - DomainFilter endpoint.DomainFilter - DryRun bool - Workers int + // DomainFilter is a list with domains that will be affected. If it is empty all available domains will be affected. + DomainFilter endpoint.DomainFilter + // DryRun is a flag specifies user's wish to run without requests to the DNS API + DryRun bool + // Workers is a number of goroutines that will create requests to the DNS API. + Workers int } //go:generate mockgen -destination=./mock/keystone_provider.go -source=./config.go KeystoneProvider diff --git a/internal/selprovider/provider.go b/internal/selprovider/provider.go index ce5437f..6f4854c 100644 --- a/internal/selprovider/provider.go +++ b/internal/selprovider/provider.go @@ -1,10 +1,8 @@ package selprovider import ( - "net/http" - domains "github.com/selectel/domains-go/pkg/v2" - "github.com/selectel/external-dns-stackit-webhook/pkg/httpclient" + "github.com/selectel/external-dns-stackit-webhook/pkg/httpdefault" "go.uber.org/zap" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/provider" @@ -23,7 +21,7 @@ type Provider struct { rrSetFetcherClient *rrSetFetcher } -// getDomainsClient returns v2.DNSClient with provided keystone and user-agent from httpclient.DefaultUserAgent. +// getDomainsClient returns v2.DNSClient with provided keystone and user-agent from httpdefault.UserAgent. func (p *Provider) getDomainsClient() (domains.DNSClient[domains.Zone, domains.RRSet], error) { token, err := p.keystoneProvider.GetToken() if err != nil { @@ -32,10 +30,9 @@ func (p *Provider) getDomainsClient() (domains.DNSClient[domains.Zone, domains.R return nil, err } - httpClient := httpclient.Default() - headers := http.Header{} + httpClient := httpdefault.Client() + headers := httpdefault.Headers() headers.Add("X-Auth-Token", token) - headers.Add("User-Agent", httpclient.DefaultUserAgent) return domains.NewClient(p.endpoint, &httpClient, headers), nil } diff --git a/pkg/httpclient/default.go b/pkg/httpclient/default.go deleted file mode 100644 index bf2a01f..0000000 --- a/pkg/httpclient/default.go +++ /dev/null @@ -1,54 +0,0 @@ -package httpclient - -import ( - "net" - "net/http" - "time" -) - -const ( - - // DefaultUserAgent of CLI represents HTTP User-Agent header that will be added to auth requests. - DefaultUserAgent = "external-dns-selectel-webhook" - - // defaultHTTPTimeout represents the default timeout (in seconds) for HTTP requests. - defaultHTTPTimeout = 30 - - // defaultDialTimeout represents the default timeout (in seconds) for HTTP connection establishments. - defaultDialTimeout = 60 - - // defaultKeepaliveTimeout represents the default keep-alive period for an active network connection. - defaultKeepaliveTimeout = 60 - - // defaultMaxIdleConns represents the maximum number of idle (keep-alive) connections. - defaultMaxIdleConns = 100 - - // defaultIdleConnTimeout represents the maximum amount of time an idle (keep-alive) connection will remain - // idle before closing itself. - defaultIdleConnTimeout = 100 - - // defaultTLSHandshakeTimeout represents the default timeout (in seconds) for TLS handshake. - defaultTLSHandshakeTimeout = 60 - - // defaultExpectContinueTimeout represents the default amount of time to wait for a server's first - // response headers. - defaultExpectContinueTimeout = 1 -) - -// Default sets up default http client for authentication. -func Default() http.Client { - return http.Client{ - Timeout: defaultHTTPTimeout * time.Second, - Transport: &http.Transport{ - Proxy: http.ProxyFromEnvironment, - DialContext: (&net.Dialer{ - Timeout: defaultDialTimeout * time.Second, - KeepAlive: defaultKeepaliveTimeout * time.Second, - }).DialContext, - MaxIdleConns: defaultMaxIdleConns, - IdleConnTimeout: defaultIdleConnTimeout * time.Second, - TLSHandshakeTimeout: defaultTLSHandshakeTimeout * time.Second, - ExpectContinueTimeout: defaultExpectContinueTimeout * time.Second, - }, - } -} diff --git a/pkg/httpdefault/default.go b/pkg/httpdefault/default.go new file mode 100644 index 0000000..355712b --- /dev/null +++ b/pkg/httpdefault/default.go @@ -0,0 +1,62 @@ +package httpdefault + +import ( + "net" + "net/http" + "time" +) + +// UserAgent represents HTTP User-Agent header that should be added to requests to Selectel API. +// You should add this header by yourself or use Headers function. +const UserAgent = "external-dns-selectel-webhook" + +const ( + // httpTimeout represents the default timeout (in seconds) for HTTP requests. + httpTimeout = 30 + + // dialTimeout represents the default timeout (in seconds) for HTTP connection establishments. + dialTimeout = 60 + + // keepaliveTimeout represents the default keep-alive period for an active network connection. + keepaliveTimeout = 60 + + // maxIdleConns represents the maximum number of idle (keep-alive) connections. + maxIdleConns = 100 + + // idleConnTimeout represents the maximum amount of time an idle (keep-alive) connection will remain + // idle before closing itself. + idleConnTimeout = 100 + + // tlsHandshakeTimeout represents the default timeout (in seconds) for TLS handshake. + tlsHandshakeTimeout = 60 + + // expectContinueTimeout represents the default amount of time to wait for a server's first response headers. + expectContinueTimeout = 5 +) + +// Client returns default HTTP client for requests to Selectel API. It does not add User-Agent header, so +// you should add it by yourself (by default it is UserAgent) or use Headers function. +func Client() http.Client { + return http.Client{ + Timeout: httpTimeout * time.Second, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: dialTimeout * time.Second, + KeepAlive: keepaliveTimeout * time.Second, + }).DialContext, + MaxIdleConns: maxIdleConns, + IdleConnTimeout: idleConnTimeout * time.Second, + TLSHandshakeTimeout: tlsHandshakeTimeout * time.Second, + ExpectContinueTimeout: expectContinueTimeout * time.Second, + }, + } +} + +// Headers returns default HTTP headers for requests to Selectel API. +func Headers() http.Header { + h := http.Header{} + h.Add("User-Agent", UserAgent) + + return h +} diff --git a/pkg/keystone/provider.go b/pkg/keystone/provider.go index d5ca68b..5866c20 100644 --- a/pkg/keystone/provider.go +++ b/pkg/keystone/provider.go @@ -3,28 +3,28 @@ package keystone import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" - "github.com/selectel/external-dns-stackit-webhook/pkg/httpclient" + "github.com/selectel/external-dns-stackit-webhook/pkg/httpdefault" "go.uber.org/zap" ) func defaultOSClient(endpoint string) (*gophercloud.ProviderClient, error) { client, err := openstack.NewClient(endpoint) - client.HTTPClient = httpclient.Default() - client.UserAgent.Prepend(httpclient.DefaultUserAgent) + client.HTTPClient = httpdefault.Client() + client.UserAgent.Prepend(httpdefault.UserAgent) return client, err } type Credentials struct { - // IdentityEndpoint is OS_AUTH_URL variable from rc.sh. + // IdentityEndpoint is an API endpoint to authorization. It is OS_AUTH_URL variable from rc.sh. IdentityEndpoint string - // AccountID is OS_PROJECT_DOMAIN_NAME variable from rc.sh. + // AccountID is Selectel account ID of the user. It is OS_PROJECT_DOMAIN_NAME variable from rc.sh. AccountID string - // IdentityEndpoint is OS_PROJECT_ID variable from rc.sh. + // ProjectID is Selectel project ID of the user. It is OS_PROJECT_ID variable from rc.sh. ProjectID string - // IdentityEndpoint is OS_USERNAME variable from rc.sh. + // Username is service user's name. It is OS_USERNAME variable from rc.sh. Username string - // IdentityEndpoint is OS_PASSWORD variable from rc.sh. + // Password is service user's password. It is OS_PASSWORD variable from rc.sh. Password string } @@ -34,7 +34,8 @@ type Provider struct { credentials Credentials } -// GetToken returns valid keystone token. It will be stored for the next requests and then checked whether it is expired. +// GetToken returns keystone token that may be used to authorize requests to Selectel API. +// It generates new token for each call. func (p Provider) GetToken() (string, error) { p.logger.Info( "getting keystone token", From bdefab2b831382379ab7697786af1e1118a8e5ec Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 22 Mar 2024 16:14:48 +0300 Subject: [PATCH 09/19] refactor: renamed stackit -> selectel --- .goreleaser.yml | 10 +++++----- Dockerfile | 4 ++-- Makefile | 6 +++--- README.md | 20 ++++++++++---------- cmd/webhook/cmd/root.go | 10 +++++----- cmd/webhook/main.go | 2 +- go.mod | 5 +---- go.sum | 8 -------- internal/selprovider/provider.go | 2 +- internal/selprovider/records_test.go | 2 +- pkg/api/adjust_endpoints_test.go | 4 ++-- pkg/api/api.go | 2 +- pkg/api/api_test.go | 4 ++-- pkg/api/apply_changes_test.go | 4 ++-- pkg/api/domain_filter_test.go | 4 ++-- pkg/api/health_test.go | 4 ++-- pkg/api/metrics.go | 2 +- pkg/api/metrics_test.go | 6 +++--- pkg/api/records_test.go | 4 ++-- pkg/keystone/provider.go | 2 +- 20 files changed, 47 insertions(+), 58 deletions(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 2b85537..ea4f8fd 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,8 +1,8 @@ -project_name: external-dns-stackit-webhook +project_name: external-dns-selectel-webhook snapshot: name_template: '{{ .Tag }}-SNAPSHOT' builds: - - id: external-dns-stackit-webhook + - id: external-dns-selectel-webhook goos: - linux - windows @@ -11,7 +11,7 @@ builds: - amd64 - arm64 main: ./cmd/webhook - binary: external-dns-stackit-webhook + binary: external-dns-selectel-webhook env: - CGO_ENABLED=0 ldflags: @@ -36,7 +36,7 @@ archives: - goos: windows format: zip dockers: - - id: external-dns-stackit-webhook + - id: external-dns-selectel-webhook use: buildx image_templates: - "{{ .Env.REGISTRY }}/{{ .Env.IMAGE_NAME }}:{{ .Tag }}" @@ -46,7 +46,7 @@ dockers: goarch: amd64 build_flag_templates: - --label=org.opencontainers.image.title={{ .ProjectName }} - - --label=org.opencontainers.image.description=stackit DNS webhook for external-dns + - --label=org.opencontainers.image.description=selectel DNS webhook for external-dns - --label=org.opencontainers.image.url=https://{{ .Env.GITHUB_SERVER_URL }}/{{ .Env.GITHUB_REPOSITORY}} - --label=org.opencontainers.image.source=https://{{ .Env.GITHUB_SERVER_URL }}/{{ .Env.GITHUB_REPOSITORY}} - --label=org.opencontainers.image.version={{ .Version }} diff --git a/Dockerfile b/Dockerfile index 602ced9..928ff73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ FROM gcr.io/distroless/static-debian11:nonroot -COPY external-dns-stackit-webhook /external-dns-stackit-webhook +COPY external-dns-selectel-webhook /external-dns-selectel-webhook -ENTRYPOINT ["/external-dns-stackit-webhook"] +ENTRYPOINT ["/external-dns-selectel-webhook"] diff --git a/Makefile b/Makefile index 72b1a77..6a27634 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ GOLANGCI_VERSION = 1.53.3 LICENCES_IGNORE_LIST = $(shell cat licenses/licenses-ignore-list.txt) VERSION ?= 0.0.1 -IMAGE_TAG_BASE ?= selectel/external-dns-stackit-webhook +IMAGE_TAG_BASE ?= selectel/external-dns-selectel-webhook IMG ?= $(IMAGE_TAG_BASE):$(VERSION) BUILD_VERSION ?= $(shell git branch --show-current) @@ -17,7 +17,7 @@ download: .PHONY: build build: - CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/external-dns-stackit-webhook -v cmd/webhook/main.go + CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/external-dns-selectel-webhook -v cmd/webhook/main.go .PHONY: docker-build docker-build: @@ -67,7 +67,7 @@ $(GO_RELEASER): .PHONY: release-check release-check: $(GO_RELEASER) ## Check if the release will work - GITHUB_SERVER_URL=github.com GITHUB_REPOSITORY=stackitcloud/external-dns-stackit-webhook REGISTRY=$(REGISTRY) IMAGE_NAME=$(IMAGE_NAME) $(GO_RELEASER) release --snapshot --clean --skip-publish + GITHUB_SERVER_URL=github.com GITHUB_REPOSITORY=selectel/external-dns-selectel-webhook REGISTRY=$(REGISTRY) IMAGE_NAME=$(IMAGE_NAME) $(GO_RELEASER) release --snapshot --clean --skip-publish GO_LICENSES = bin/go-licenses $(GO_LICENSES): diff --git a/README.md b/README.md index 30b8b1c..72c8e21 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # Selectel Webhook - ExternalDNS [![GoTemplate](https://img.shields.io/badge/go/template-black?logo=go)](https://github.com/golang-standards/project-layout) -[![CI](https://github.com/selectel/external-dns-stackit-webhook/actions/workflows/main.yml/badge.svg)](https://github.com/selectel/external-dns-stackit-webhook/actions/workflows/main.yml) -[![Go Report Card](https://goreportcard.com/badge/github.com/selectel/external-dns-stackit-webhook)](https://goreportcard.com/report/github.com/selectel/external-dns-stackit-webhook) -[![GitHub release](https://img.shields.io/github/release/selectel/external-dns-stackit-webhook.svg)](https://github.com/selectel/external-dns-stackit-webhook/releases) -[![Last Commit](https://img.shields.io/github/last-commit/selectel/external-dns-stackit-webhook/main.svg)](https://github.com/selectel/external-dns-stackit-webhook/commits/main) -[![GitHub issues](https://img.shields.io/github/issues/selectel/external-dns-stackit-webhook.svg)](https://github.com/selectel/external-dns-stackit-webhook/issues) -[![GitHub pull requests](https://img.shields.io/github/issues-pr/selectel/external-dns-stackit-webhook.svg)](https://github.com/selectel/external-dns-stackit-webhook/pulls) -[![GitHub stars](https://img.shields.io/github/stars/selectel/external-dns-stackit-webhook.svg?style=social&label=Star&maxAge=2592000)](https://github.com/selectel/external-dns-stackit-webhook/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/selectel/external-dns-stackit-webhook.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/selectel/external-dns-stackit-webhook/network) +[![CI](https://github.com/selectel/external-dns-selectel-webhook/actions/workflows/main.yml/badge.svg)](https://github.com/selectel/external-dns-selectel-webhook/actions/workflows/main.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/selectel/external-dns-selectel-webhook)](https://goreportcard.com/report/github.com/selectel/external-dns-selectel-webhook) +[![GitHub release](https://img.shields.io/github/release/selectel/external-dns-selectel-webhook.svg)](https://github.com/selectel/external-dns-selectel-webhook/releases) +[![Last Commit](https://img.shields.io/github/last-commit/selectel/external-dns-selectel-webhook/main.svg)](https://github.com/selectel/external-dns-selectel-webhook/commits/main) +[![GitHub issues](https://img.shields.io/github/issues/selectel/external-dns-selectel-webhook.svg)](https://github.com/selectel/external-dns-selectel-webhook/issues) +[![GitHub pull requests](https://img.shields.io/github/issues-pr/selectel/external-dns-selectel-webhook.svg)](https://github.com/selectel/external-dns-selectel-webhook/pulls) +[![GitHub stars](https://img.shields.io/github/stars/selectel/external-dns-selectel-webhook.svg?style=social&label=Star&maxAge=2592000)](https://github.com/selectel/external-dns-selectel-webhook/stargazers) +[![GitHub forks](https://img.shields.io/github/forks/selectel/external-dns-selectel-webhook.svg?style=social&label=Fork&maxAge=2592000)](https://github.com/selectel/external-dns-selectel-webhook/network) ExternalDNS serves as an add-on for Kubernetes designed to automate the management of Domain Name System (DNS) records for Kubernetes services by utilizing various DNS providers. While Kubernetes traditionally manages DNS @@ -24,7 +24,7 @@ to create and read dns zones, and finally, establish a Selectel zone. ## Kubernetes Deployment The Selectel webhook is presented as a standard Open Container Initiative (OCI) image released in the -[GitHub container registry](https://github.com/selectel/external-dns-stackit-webhook/pkgs/container/external-dns-stackit-webhook). +[GitHub container registry](https://github.com/selectel/external-dns-selectel-webhook/pkgs/container/external-dns-selectel-webhook). The deployment is compatible with all Kubernetes-supported methods. The subsequent example demonstrates the deployment as a [sidecar container](https://kubernetes.io/docs/concepts/workloads/pods/#workload-resources-for-managing-pods) @@ -175,7 +175,7 @@ spec: readOnlyRootFilesystem: true runAsNonRoot: true runAsUser: 65534 - image: ghcr.io/selectel/external-dns-stackit-webhook:v0.1.0 + image: ghcr.io/selectel/external-dns-selectel-webhook:v0.1.0 imagePullPolicy: IfNotPresent args: - --project-id=c158c736-0300-4044-95c4-b7d404279b35 # your project id diff --git a/cmd/webhook/cmd/root.go b/cmd/webhook/cmd/root.go index a98c56e..e88b7ba 100644 --- a/cmd/webhook/cmd/root.go +++ b/cmd/webhook/cmd/root.go @@ -5,10 +5,10 @@ import ( "log" "strings" - "github.com/selectel/external-dns-stackit-webhook/internal/selprovider" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - "github.com/selectel/external-dns-stackit-webhook/pkg/keystone" - "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" + "github.com/selectel/external-dns-selectel-webhook/internal/selprovider" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + "github.com/selectel/external-dns-selectel-webhook/pkg/keystone" + "github.com/selectel/external-dns-selectel-webhook/pkg/metrics" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -32,7 +32,7 @@ var ( ) var rootCmd = &cobra.Command{ - Use: "external-dns-stackit-webhook", + Use: "external-dns-selectel-webhook", Short: "provider webhook for the Selectel DNS service", Long: "provider webhook for the Selectel DNS service", Run: func(cmd *cobra.Command, args []string) { diff --git a/cmd/webhook/main.go b/cmd/webhook/main.go index 95a48e3..90fd822 100644 --- a/cmd/webhook/main.go +++ b/cmd/webhook/main.go @@ -1,6 +1,6 @@ package main -import "github.com/selectel/external-dns-stackit-webhook/cmd/webhook/cmd" +import "github.com/selectel/external-dns-selectel-webhook/cmd/webhook/cmd" func main() { err := cmd.Execute() diff --git a/go.mod b/go.mod index c45e6f9..edfa29c 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/selectel/external-dns-stackit-webhook +module github.com/selectel/external-dns-selectel-webhook go 1.20 @@ -12,8 +12,6 @@ require ( github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.17.0 - github.com/stackitcloud/stackit-sdk-go/core v0.10.1 - github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4 github.com/stretchr/testify v1.8.4 go.uber.org/mock v0.3.0 go.uber.org/zap v1.26.0 @@ -29,7 +27,6 @@ require ( github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect diff --git a/go.sum b/go.sum index 9af9c75..a2172de 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,6 @@ github.com/gofiber/fiber/v2 v2.52.2 h1:b0rYH6b06Df+4NyrbdptQL8ifuxw/Tf2DgfkZkDax github.com/gofiber/fiber/v2 v2.52.2/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -150,8 +148,6 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gophercloud/gophercloud v1.5.0 h1:cDN6XFCLKiiqvYpjQLq9AiM7RDRbIC9450WpPH+yvXo= -github.com/gophercloud/gophercloud v1.5.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/gophercloud/gophercloud v1.11.0 h1:ls0O747DIq1D8SUHc7r2vI8BFbMLeLFuENaAIfEx7OM= github.com/gophercloud/gophercloud v1.11.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -242,10 +238,6 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stackitcloud/stackit-sdk-go/core v0.10.1 h1:lzyualywD/2xIsYUHwlqCurG1OwlqCJVtJbOcPO6OzE= -github.com/stackitcloud/stackit-sdk-go/core v0.10.1/go.mod h1:mDX1mSTsB3mP+tNBGcFNx6gH1mGBN4T+dVt+lcw7nlw= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4 h1:n/X2pVdETDXGHk+vCsg0p3b2zGxSRMJ065to/aAoncg= -github.com/stackitcloud/stackit-sdk-go/services/dns v0.8.4/go.mod h1:PvgUVFLgELRADWk2epZdCryk0fs8b4DN47ghEJjNWhk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= diff --git a/internal/selprovider/provider.go b/internal/selprovider/provider.go index 6f4854c..22e5f68 100644 --- a/internal/selprovider/provider.go +++ b/internal/selprovider/provider.go @@ -2,7 +2,7 @@ package selprovider import ( domains "github.com/selectel/domains-go/pkg/v2" - "github.com/selectel/external-dns-stackit-webhook/pkg/httpdefault" + "github.com/selectel/external-dns-selectel-webhook/pkg/httpdefault" "go.uber.org/zap" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/provider" diff --git a/internal/selprovider/records_test.go b/internal/selprovider/records_test.go index f20c41a..b02ff35 100644 --- a/internal/selprovider/records_test.go +++ b/internal/selprovider/records_test.go @@ -9,7 +9,7 @@ import ( "github.com/goccy/go-json" domains "github.com/selectel/domains-go/pkg/v2" - mock_selprovider "github.com/selectel/external-dns-stackit-webhook/internal/selprovider/mock" + mock_selprovider "github.com/selectel/external-dns-selectel-webhook/internal/selprovider/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/adjust_endpoints_test.go b/pkg/api/adjust_endpoints_test.go index 311a90d..92075be 100644 --- a/pkg/api/adjust_endpoints_test.go +++ b/pkg/api/adjust_endpoints_test.go @@ -8,8 +8,8 @@ import ( "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-selectel-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/api.go b/pkg/api/api.go index ef6a90c..0ef7e46 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -15,7 +15,7 @@ import ( fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/pprof" fiberrecover "github.com/gofiber/fiber/v2/middleware/recover" - "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" + "github.com/selectel/external-dns-selectel-webhook/pkg/metrics" "go.uber.org/zap" "sigs.k8s.io/external-dns/provider" ) diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go index ed5a6f4..9729a6f 100644 --- a/pkg/api/api_test.go +++ b/pkg/api/api_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-selectel-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/apply_changes_test.go b/pkg/api/apply_changes_test.go index 917149f..4cbc64b 100644 --- a/pkg/api/apply_changes_test.go +++ b/pkg/api/apply_changes_test.go @@ -9,8 +9,8 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-selectel-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/domain_filter_test.go b/pkg/api/domain_filter_test.go index 24a031b..5ef6c39 100644 --- a/pkg/api/domain_filter_test.go +++ b/pkg/api/domain_filter_test.go @@ -8,8 +8,8 @@ import ( "github.com/goccy/go-json" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-selectel-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/health_test.go b/pkg/api/health_test.go index ea6093b..629523c 100644 --- a/pkg/api/health_test.go +++ b/pkg/api/health_test.go @@ -6,8 +6,8 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-selectel-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index b50c449..7f2a22e 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -7,7 +7,7 @@ import ( "github.com/gofiber/adaptor/v2" "github.com/gofiber/fiber/v2" "github.com/prometheus/client_golang/prometheus/promhttp" - metrics_collector "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" + metrics_collector "github.com/selectel/external-dns-selectel-webhook/pkg/metrics" ) // registerAt registers the metrics endpoint. diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index dc11cab..796ed3b 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/gofiber/fiber/v2" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - metrics_collector "github.com/selectel/external-dns-stackit-webhook/pkg/metrics" - mock_metrics_collector "github.com/selectel/external-dns-stackit-webhook/pkg/metrics/mock" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + metrics_collector "github.com/selectel/external-dns-selectel-webhook/pkg/metrics" + mock_metrics_collector "github.com/selectel/external-dns-selectel-webhook/pkg/metrics/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) diff --git a/pkg/api/records_test.go b/pkg/api/records_test.go index 39b4a40..3740f8f 100644 --- a/pkg/api/records_test.go +++ b/pkg/api/records_test.go @@ -7,8 +7,8 @@ import ( "testing" "github.com/goccy/go-json" - "github.com/selectel/external-dns-stackit-webhook/pkg/api" - mock_provider "github.com/selectel/external-dns-stackit-webhook/pkg/api/mock" + "github.com/selectel/external-dns-selectel-webhook/pkg/api" + mock_provider "github.com/selectel/external-dns-selectel-webhook/pkg/api/mock" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" "go.uber.org/zap" diff --git a/pkg/keystone/provider.go b/pkg/keystone/provider.go index 5866c20..feff8b5 100644 --- a/pkg/keystone/provider.go +++ b/pkg/keystone/provider.go @@ -3,7 +3,7 @@ package keystone import ( "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" - "github.com/selectel/external-dns-stackit-webhook/pkg/httpdefault" + "github.com/selectel/external-dns-selectel-webhook/pkg/httpdefault" "go.uber.org/zap" ) From a28758ae1f0d2a98246090035a7771d608dfbc96 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Thu, 28 Mar 2024 16:19:00 +0300 Subject: [PATCH 10/19] fix: typo --- internal/selprovider/apply_changes_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/selprovider/apply_changes_test.go b/internal/selprovider/apply_changes_test.go index 210660f..3279aed 100644 --- a/internal/selprovider/apply_changes_test.go +++ b/internal/selprovider/apply_changes_test.go @@ -40,7 +40,7 @@ func TestApplyChanges(t *testing.T) { func testApplyChanges(t *testing.T, changeType ChangeType) { t.Helper() ctx := context.Background() - validZoneResponse := getValidResponseZoneALlBytes(t) + validZoneResponse := getValidResponseZoneAllBytes(t) validRRSetResponse := getValidResponseRRSetAllBytes(t) invalidZoneResponse := []byte(`{"invalid: "json"`) @@ -83,7 +83,7 @@ func TestNoMatchingZoneFound(t *testing.T) { t.Parallel() ctx := context.Background() - validZoneResponse := getValidResponseZoneALlBytes(t) + validZoneResponse := getValidResponseZoneAllBytes(t) mux := http.NewServeMux() server := httptest.NewServer(mux) @@ -111,7 +111,7 @@ func TestNoRRSetFound(t *testing.T) { t.Parallel() ctx := context.Background() - validZoneResponse := getValidResponseZoneALlBytes(t) + validZoneResponse := getValidResponseZoneAllBytes(t) rrSets := getValidResponseRRSetAll() rrSets.GetItems()[0].Name = "notfound.test.com" validRRSetResponse, err := json.Marshal(rrSets) @@ -327,7 +327,7 @@ func responseHandler(responseBody []byte, statusCode int) func(http.ResponseWrit } } -func getValidResponseZoneALlBytes(t *testing.T) []byte { +func getValidResponseZoneAllBytes(t *testing.T) []byte { t.Helper() zones := getValidZoneResponseAll() From ad84d6726425e9017e847e26026165d196b87730 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Thu, 28 Mar 2024 17:43:47 +0300 Subject: [PATCH 11/19] fix: created constants & wrapped authorization error --- cmd/webhook/cmd/root.go | 11 +++++++++-- internal/selprovider/constants.go | 9 +++++++++ internal/selprovider/provider.go | 2 +- internal/selprovider/rrset_fetcher.go | 6 +++--- internal/selprovider/zone_fetcher.go | 6 +++--- pkg/keystone/provider.go | 17 ++++++++++++++--- 6 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 internal/selprovider/constants.go diff --git a/cmd/webhook/cmd/root.go b/cmd/webhook/cmd/root.go index e88b7ba..16b5728 100644 --- a/cmd/webhook/cmd/root.go +++ b/cmd/webhook/cmd/root.go @@ -31,6 +31,13 @@ var ( logLevel string ) +const ( + // DefaultAuthURL represents default endpoint where keystone-token should be retrieved. + DefaultAuthURL = "https://cloud.api.selcloud.ru/identity/v3" + // DefaultDomainsURL represents default endpoint of Selectel DNS API. + DefaultDomainsURL = "https://api.selectel.ru/domains/v2" +) + var rootCmd = &cobra.Command{ Use: "external-dns-selectel-webhook", Short: "provider webhook for the Selectel DNS service", @@ -114,10 +121,10 @@ func init() { cobra.OnInitialize(initConfig) rootCmd.PersistentFlags().StringVar(&apiPort, "api-port", "8888", "Specifies the port to listen on.") - rootCmd.PersistentFlags().StringVar(&baseURL, "base-url", "https://api.selectel.ru/domains/v2", "Identifies the Base URL for utilizing the API.") + rootCmd.PersistentFlags().StringVar(&baseURL, "base-url", DefaultDomainsURL, "Identifies the Base URL for utilizing the API.") rootCmd.PersistentFlags().StringVar(&projectID, "project-id", "", "Specifies the project id to authorize.") rootCmd.PersistentFlags().StringVar(&accountID, "account-id", "", "Specifies the account id to authorize.") - rootCmd.PersistentFlags().StringVar(&authorizationURL, "auth-url", "https://cloud.api.selcloud.ru/identity/v3", "Identifies the URL for utilizing the API to receive keystone-token.") + rootCmd.PersistentFlags().StringVar(&authorizationURL, "auth-url", DefaultAuthURL, "Identifies the URL for utilizing the API to receive keystone-token.") rootCmd.PersistentFlags().StringVar(&username, "username", "", "Specifies the username of service user to authorize.") rootCmd.PersistentFlags().StringVar(&password, "password", "", "Specifies the password of service user to authorize.") rootCmd.PersistentFlags().IntVar(&worker, "worker", 10, "Specifies the number "+ diff --git a/internal/selprovider/constants.go b/internal/selprovider/constants.go new file mode 100644 index 0000000..ae143fa --- /dev/null +++ b/internal/selprovider/constants.go @@ -0,0 +1,9 @@ +package selprovider + +const ( + domainsOptionLimit = "limit" + domainsOptionOffset = "offset" + + defaultDomainsLimit = "1000" + defaultDomainsOffset = "0" +) diff --git a/internal/selprovider/provider.go b/internal/selprovider/provider.go index 22e5f68..0dbe0cb 100644 --- a/internal/selprovider/provider.go +++ b/internal/selprovider/provider.go @@ -25,7 +25,7 @@ type Provider struct { func (p *Provider) getDomainsClient() (domains.DNSClient[domains.Zone, domains.RRSet], error) { token, err := p.keystoneProvider.GetToken() if err != nil { - p.logger.Error("authorization error during getting keystone token", zap.Error(err)) + p.logger.Error("failed to get keystone token", zap.Error(err)) return nil, err } diff --git a/internal/selprovider/rrset_fetcher.go b/internal/selprovider/rrset_fetcher.go index c73af14..3ba085f 100644 --- a/internal/selprovider/rrset_fetcher.go +++ b/internal/selprovider/rrset_fetcher.go @@ -32,8 +32,8 @@ func (r *rrSetFetcher) fetchRecords( zoneId string, options map[string]string, ) ([]*domains.RRSet, error) { - options["limit"] = "1000" - options["offset"] = "0" + options[domainsOptionLimit] = defaultDomainsLimit + options[domainsOptionOffset] = defaultDomainsOffset var rrSets []*domains.RRSet @@ -45,7 +45,7 @@ func (r *rrSetFetcher) fetchRecords( rrSets = append(rrSets, rrSetsResponse.GetItems()...) - options["offset"] = strconv.Itoa(rrSetsResponse.GetNextOffset()) + options[domainsOptionOffset] = strconv.Itoa(rrSetsResponse.GetNextOffset()) if rrSetsResponse.GetNextOffset() == 0 { break } diff --git a/internal/selprovider/zone_fetcher.go b/internal/selprovider/zone_fetcher.go index 29662b6..7d7231f 100644 --- a/internal/selprovider/zone_fetcher.go +++ b/internal/selprovider/zone_fetcher.go @@ -52,8 +52,8 @@ func (z *zoneFetcher) fetchZones( client domains.DNSClient[domains.Zone, domains.RRSet], options map[string]string, ) ([]*domains.Zone, error) { - options["limit"] = "1000" - options["offset"] = "0" + options[domainsOptionLimit] = defaultDomainsLimit + options[domainsOptionOffset] = defaultDomainsOffset var zones []*domains.Zone @@ -65,7 +65,7 @@ func (z *zoneFetcher) fetchZones( zones = append(zones, zonesResponse.GetItems()...) - options["offset"] = strconv.Itoa(zonesResponse.GetNextOffset()) + options[domainsOptionOffset] = strconv.Itoa(zonesResponse.GetNextOffset()) if zonesResponse.GetNextOffset() == 0 { break } diff --git a/pkg/keystone/provider.go b/pkg/keystone/provider.go index feff8b5..d5c64d9 100644 --- a/pkg/keystone/provider.go +++ b/pkg/keystone/provider.go @@ -1,12 +1,19 @@ package keystone import ( + "fmt" + "github.com/gophercloud/gophercloud" "github.com/gophercloud/gophercloud/openstack" "github.com/selectel/external-dns-selectel-webhook/pkg/httpdefault" "go.uber.org/zap" ) +var ( + errFailedCreateClientFmt = "failed to create default openstack client: %w" + errAuthorizationFailedFmt = "authorization failed: %w" +) + func defaultOSClient(endpoint string) (*gophercloud.ProviderClient, error) { client, err := openstack.NewClient(endpoint) client.HTTPClient = httpdefault.Client() @@ -34,7 +41,7 @@ type Provider struct { credentials Credentials } -// GetToken returns keystone token that may be used to authorize requests to Selectel API. +// GetToken returns keystone token that may be used to authorize requests to Selectel API in the project scope. // It generates new token for each call. func (p Provider) GetToken() (string, error) { p.logger.Info( @@ -60,11 +67,15 @@ func (p Provider) GetToken() (string, error) { if err != nil { p.logger.Error("error during creating default openstack client", zap.Error(err)) - return "", err + return "", fmt.Errorf(errFailedCreateClientFmt, err) } + err = openstack.Authenticate(client, opts) + if err != nil { + return "", fmt.Errorf(errAuthorizationFailedFmt, err) + } - return client.Token(), err + return client.Token(), nil } func NewProvider(logger *zap.Logger, credentials Credentials) *Provider { From 01f7d9d25f83854b9622b0e289c4af7f72d87526 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Thu, 28 Mar 2024 18:04:32 +0300 Subject: [PATCH 12/19] feat: added link to list of selectel api urls --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 72c8e21..4f3d13b 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,9 @@ Below are the options that are available. need to iterate over all zones and records, it can be parallelized. However, it is important to avoid setting this number excessively high to prevent receiving 429 rate limiting from the API (default 10). - `--base-url`/`BASE_URL` (optional): Identifies the Base URL for utilizing the API ( - default "https://api.selectel.ru/domains/v2"). + default "https://api.selectel.ru/domains/v2"). The full list of Selectel API URLs you can see [here](https://developers.selectel.ru/docs/control-panel/urls/). +- `--auth-url`/`AUTH_URL` (optional): Identifies the URL for utilizing the API to receive keystone-token ( + default "https://cloud.api.selcloud.ru/identity/v3"). - `--api-port`/`API_PORT` (optional): Specifies the port to listen on (default 8888). - `--domain-filter`/`DOMAIN_FILER` (optional): Establishes a filter for DNS zone names (default []). - `--dry-run`/`DRY_RUN` (optional): Specifies whether to perform a dry run (default false). From 045cff35a3dd930ee7d972247153d13e67a8a030 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Thu, 28 Mar 2024 18:16:47 +0300 Subject: [PATCH 13/19] fix: removed redundant debug log --- internal/selprovider/apply_changes.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/selprovider/apply_changes.go b/internal/selprovider/apply_changes.go index af7173f..e90e13f 100644 --- a/internal/selprovider/apply_changes.go +++ b/internal/selprovider/apply_changes.go @@ -71,8 +71,6 @@ func (p *Provider) deleteRRSets( endpoints []*endpoint.Endpoint, ) error { if len(endpoints) == 0 { - p.logger.Debug("no endpoints to delete") - return nil } From 5e97ce14cac939e68a8e6ffa3f7e61e5f3887062 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Fri, 29 Mar 2024 15:13:36 +0300 Subject: [PATCH 14/19] fix: made log lowercase --- cmd/webhook/cmd/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/webhook/cmd/root.go b/cmd/webhook/cmd/root.go index 16b5728..6300ed8 100644 --- a/cmd/webhook/cmd/root.go +++ b/cmd/webhook/cmd/root.go @@ -47,7 +47,7 @@ var rootCmd = &cobra.Command{ defer func(logger *zap.Logger) { err := logger.Sync() if err != nil { - log.Printf("Synchronization of logs failed with error: %v", err) + log.Printf("synchronization of logs failed with error: %v", err) } }(logger) From f7270f076e4cdf41351a470ede5222abc2a39bfb Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Mon, 1 Apr 2024 14:38:28 +0300 Subject: [PATCH 15/19] fix: docs in readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 4f3d13b..ec96a6b 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ of your Selectel domains within your Kubernetes cluster using [ExternalDNS](https://github.com/kubernetes-sigs/external-dns). For utilizing ExternalDNS with Selectel, it is mandatory to establish a Selectel project, a service account -within the project, generate an authentication token for the service account, authorize the service account -to create and read dns zones, and finally, establish a Selectel zone. +within the project, and finally, establish a Selectel zone. ## Kubernetes Deployment @@ -215,7 +214,7 @@ EOF ## Configuration The configuration of the Selectel webhook can be accomplished through command line arguments and environment variables. -Below are the options that are available. +Below are the options that are available in format `--cli-argument`/`ENVIRONMENT_VARIABLE`. - `--project-id`/`PROJECT_ID` (required): Specifies the project id to authorize. - `--account-id`/`ACCOUNT_ID` (required): Specifies the account id to authorize. @@ -224,10 +223,11 @@ Below are the options that are available. - `--worker`/`WORKER` (optional): Specifies the number of workers to employ for querying the API. Given that we need to iterate over all zones and records, it can be parallelized. However, it is important to avoid setting this number excessively high to prevent receiving 429 rate limiting from the API (default 10). -- `--base-url`/`BASE_URL` (optional): Identifies the Base URL for utilizing the API ( - default "https://api.selectel.ru/domains/v2"). The full list of Selectel API URLs you can see [here](https://developers.selectel.ru/docs/control-panel/urls/). -- `--auth-url`/`AUTH_URL` (optional): Identifies the URL for utilizing the API to receive keystone-token ( - default "https://cloud.api.selcloud.ru/identity/v3"). +- `--base-url`/`BASE_URL` (optional): Identifies the Base URL for utilizing the API + (default "https://api.selectel.ru/domains/v2"). The full list of Selectel API URLs you can + see [here](https://developers.selectel.ru/docs/control-panel/urls/). +- `--auth-url`/`AUTH_URL` (optional): Identifies the URL for utilizing the API to receive keystone-token + (default "https://cloud.api.selcloud.ru/identity/v3"). - `--api-port`/`API_PORT` (optional): Specifies the port to listen on (default 8888). - `--domain-filter`/`DOMAIN_FILER` (optional): Establishes a filter for DNS zone names (default []). - `--dry-run`/`DRY_RUN` (optional): Specifies whether to perform a dry run (default false). From 492a8aaf0998625c8534f1520826b01811dbfc2a Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Tue, 2 Apr 2024 11:17:09 +0300 Subject: [PATCH 16/19] refactor: moved "filter" & "name" to const --- internal/selprovider/constants.go | 2 ++ internal/selprovider/rrset_fetcher.go | 2 +- internal/selprovider/zone_fetcher.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/selprovider/constants.go b/internal/selprovider/constants.go index ae143fa..b3d6951 100644 --- a/internal/selprovider/constants.go +++ b/internal/selprovider/constants.go @@ -3,6 +3,8 @@ package selprovider const ( domainsOptionLimit = "limit" domainsOptionOffset = "offset" + domainsOptionFilter = "filter" + domainsOptionName = "name" defaultDomainsLimit = "1000" defaultDomainsOffset = "0" diff --git a/internal/selprovider/rrset_fetcher.go b/internal/selprovider/rrset_fetcher.go index 3ba085f..eaf16a6 100644 --- a/internal/selprovider/rrset_fetcher.go +++ b/internal/selprovider/rrset_fetcher.go @@ -72,7 +72,7 @@ func (r *rrSetFetcher) getRRSetForUpdateDeletion( } domainRrSets, err := r.fetchRecords(ctx, client, resultZone.ID, map[string]string{ - "name": change.DNSName, + domainsOptionName: change.DNSName, }) if err != nil { return nil, nil, err diff --git a/internal/selprovider/zone_fetcher.go b/internal/selprovider/zone_fetcher.go index 7d7231f..a11a2d1 100644 --- a/internal/selprovider/zone_fetcher.go +++ b/internal/selprovider/zone_fetcher.go @@ -35,7 +35,7 @@ func (z *zoneFetcher) zones(ctx context.Context, client domains.DNSClient[domain // send one request per filter for _, filter := range z.domainFilter.Filters { zones, err := z.fetchZones(ctx, client, map[string]string{ - "filter": filter, + domainsOptionFilter: filter, }) if err != nil { return nil, err From 47d38842562c31ccbc8ac9857e9fd1a25fdbcd08 Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Wed, 3 Apr 2024 15:16:27 +0300 Subject: [PATCH 17/19] docs: added CONTRIBUTING.md --- CONTRIBUTING.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2204142 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing to External DNS Selectel Webhook + +Welcome and thank you for making it this far and considering contributing to external-dns-selectel-webhook. +We always appreciate any contributions by raising issues, improving the documentation, fixing bugs in the CLI or adding new features. + +Before opening a PR please read through this document. + +## Process of making an addition + +To contribute any code to this repository just do the following: + +1. Make sure you have current Go's version of the project installed +2. Fork this repository +3. Run `make build` to make sure everything's setup correctly +4. Make your changes + > Please follow the [seven rules of greate Git commit messages](https://chris.beams.io/posts/git-commit/#seven-rules) + > and make sure to keep your commits clean and atomic. + > Your PR won't be squashed before merging so the commits should tell a story. + > + > Optional: Sign-off on all Git commits by running `git commit -s`. + > Take a look at the [Gihub Docs](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits) for further information. + > + > Add documentation and tests for your addition if needed. +5. Run `make lint test` to ensure your code is ready to be merged + > If any linting issues occur please fix them. + > Using a nolint directive should only be used as a last resort. +6. Open a PR and make sure the CI pipelines succeed. + > Your PR needs to have a semantic title, which can look like: `type(scope) Short description` + > All available `scopes` & `types` are defined in [semantic.yml](https://github.com/stackitcloud/external-dns-selectel-webhook/blob/main/.github/semantic.yml) + > + > An example PR tile for adding a new feature for the CLI would look like: `cli(feat) Add saving output to file` +7. Wait for two of the maintainers to review your code and react to the comments. +8. After two approvals merge the PR +9. Thank you for your contribution! :) From 4453ecb04a0502a7953d41cb771455da6b21afbf Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Wed, 3 Apr 2024 15:22:06 +0300 Subject: [PATCH 18/19] docs: fixed --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2204142..5fbf52a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Before opening a PR please read through this document. To contribute any code to this repository just do the following: -1. Make sure you have current Go's version of the project installed +1. Make sure you have a Go version installed that is similar to the version of this project 2. Fork this repository 3. Run `make build` to make sure everything's setup correctly 4. Make your changes From 43005092df59b244eb26063e218d5cce7afd47ab Mon Sep 17 00:00:00 2001 From: "belokobylskii.i" Date: Mon, 8 Apr 2024 13:12:47 +0300 Subject: [PATCH 19/19] fix: stackitcloud -> selectel --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5fbf52a..4e13acf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ To contribute any code to this repository just do the following: > Using a nolint directive should only be used as a last resort. 6. Open a PR and make sure the CI pipelines succeed. > Your PR needs to have a semantic title, which can look like: `type(scope) Short description` - > All available `scopes` & `types` are defined in [semantic.yml](https://github.com/stackitcloud/external-dns-selectel-webhook/blob/main/.github/semantic.yml) + > All available `scopes` & `types` are defined in [semantic.yml](https://github.com/selectel/external-dns-selectel-webhook/blob/main/.github/semantic.yml) > > An example PR tile for adding a new feature for the CLI would look like: `cli(feat) Add saving output to file` 7. Wait for two of the maintainers to review your code and react to the comments.