diff --git a/Makefile b/Makefile index 30d5f4b..3e27468 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -GOLANGCI_VERSION = 1.53.3 +GOLANGCI_VERSION = 1.59.1 LICENCES_IGNORE_LIST = $(shell cat licenses/licenses-ignore-list.txt) VERSION ?= 0.0.1 diff --git a/go.mod b/go.mod index 2879129..1e4dcdd 100644 --- a/go.mod +++ b/go.mod @@ -64,9 +64,9 @@ require ( 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/text v0.13.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 9be4b2e..c33448a 100644 --- a/go.sum +++ b/go.sum @@ -355,8 +355,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -419,8 +419,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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.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= @@ -433,8 +433,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/stackitprovider/apply_changes.go b/internal/stackitprovider/apply_changes.go index 9b89309..f8c0496 100644 --- a/internal/stackitprovider/apply_changes.go +++ b/internal/stackitprovider/apply_changes.go @@ -3,6 +3,7 @@ package stackitprovider import ( "context" "fmt" + "sync" stackitdnsclient "github.com/stackitcloud/stackit-sdk-go/services/dns" "go.uber.org/zap" @@ -12,43 +13,99 @@ import ( // ApplyChanges applies a given set of changes in a given zone. func (d *StackitDNSProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + var tasks []changeTask // create rr set. POST /v1/projects/{projectId}/zones/{zoneId}/rrsets - err := d.createRRSets(ctx, changes.Create) - if err != nil { - return err - } - + tasks = append(tasks, d.buildRRSetTasks(changes.Create, CREATE)...) // update rr set. PATCH /v1/projects/{projectId}/zones/{zoneId}/rrsets/{rrSetId} - err = d.updateRRSets(ctx, changes.UpdateNew) + tasks = append(tasks, d.buildRRSetTasks(changes.UpdateNew, UPDATE)...) + d.logger.Info("records to delete", zap.String("records", fmt.Sprintf("%v", changes.Delete))) + // delete rr set. DELETE /v1/projects/{projectId}/zones/{zoneId}/rrsets/{rrSetId} + tasks = append(tasks, d.buildRRSetTasks(changes.Delete, DELETE)...) + + zones, err := d.zoneFetcherClient.zones(ctx) 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 d.handleRRSetWithWorkers(ctx, tasks, zones) +} + +// handleRRSetWithWorkers handles the given endpoints with workers to optimize speed. +func (d *StackitDNSProvider) buildRRSetTasks( + endpoints []*endpoint.Endpoint, + action string, +) []changeTask { + tasks := make([]changeTask, 0, len(endpoints)) + + for _, change := range endpoints { + tasks = append(tasks, changeTask{ + action: action, + change: change, + }) } - return nil + return tasks } -// createRRSets creates new record sets in the stackitprovider for the given endpoints that are in the -// creation field. -func (d *StackitDNSProvider) createRRSets( +// handleRRSetWithWorkers handles the given endpoints with workers to optimize speed. +func (d *StackitDNSProvider) handleRRSetWithWorkers( ctx context.Context, - endpoints []*endpoint.Endpoint, + tasks []changeTask, + zones []stackitdnsclient.Zone, ) error { - if len(endpoints) == 0 { - return nil + workerChannel := make(chan changeTask, len(tasks)) + errorChannel := make(chan error, len(tasks)) + + var wg sync.WaitGroup + for i := 0; i < d.workers; i++ { + wg.Add(1) + go d.changeWorker(ctx, workerChannel, errorChannel, zones, &wg) } - zones, err := d.zoneFetcherClient.zones(ctx) - if err != nil { - return err + for _, task := range tasks { + workerChannel <- task + } + close(workerChannel) + + // capture first error + var err error + for i := 0; i < len(tasks); i++ { + err = <-errorChannel + if err != nil { + break + } + } + + // wait until all workers have finished + wg.Wait() + + return err +} + +// 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, + wg *sync.WaitGroup, +) { + defer wg.Done() + + for change := range changes { + var err error + switch change.action { + case CREATE: + err = d.createRRSet(ctx, change.change, zones) + case UPDATE: + err = d.updateRRSet(ctx, change.change, zones) + case DELETE: + err = d.deleteRRSet(ctx, change.change, zones) + } + errorChannel <- err } - return d.handleRRSetWithWorkers(ctx, endpoints, zones, CREATE) + d.logger.Debug("change worker finished") } // createRRSet creates a new record set in the stackitprovider for the given endpoint. @@ -88,24 +145,6 @@ func (d *StackitDNSProvider) createRRSet( 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, @@ -142,28 +181,6 @@ func (d *StackitDNSProvider) updateRRSet( 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, @@ -197,62 +214,3 @@ func (d *StackitDNSProvider) deleteRRSet( 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 index 769e72e..2666730 100644 --- a/internal/stackitprovider/apply_changes_test.go +++ b/internal/stackitprovider/apply_changes_test.go @@ -3,6 +3,7 @@ package stackitprovider import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "testing" @@ -143,6 +144,61 @@ func TestNoRRSetFound(t *testing.T) { assert.Error(t, err) } +func TestPartialUpdate(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) + // Set up change type-specific endpoints + // based on setUpChangeTypeEndpoints(t, mux, validRRSetResponse, http.StatusOK, Update) + // but extended to check that the rrset is updated + rrSetUpdated := false + mux.HandleFunc( + "/v1/projects/1234/zones/1234/rrsets/1234", + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + fmt.Println(r.Method) + if r.Method == http.MethodPatch { + rrSetUpdated = true + } + }, + ) + 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") + }, + ) + + stackitDnsProvider, err := getDefaultTestProvider(server) + assert.NoError(t, err) + + // Create update change + changes := getChangeTypeChanges(Update) + // Add task to create invalid endpoint + changes.Create = []*endpoint.Endpoint{ + {DNSName: "notfound.com", Targets: endpoint.Targets{"test.notfound.com"}}, + } + + err = stackitDnsProvider.ApplyChanges(ctx, changes) + assert.Error(t, err) + assert.True(t, rrSetUpdated, "rrset was not updated") +} + // 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) {