From 8c864e49d7bd5944e6df4763e7206762b3f2a0f6 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sun, 18 Jun 2023 15:05:21 +0200 Subject: [PATCH 01/19] initial plugin implementation --- docs/img/plugin-provider.png | Bin 0 -> 6733 bytes docs/tutorials/plugin-provider.md | 44 ++++ go.mod | 1 + go.sum | 2 + main.go | 26 +++ pkg/apis/externaldns/types.go | 15 +- pkg/apis/externaldns/types_test.go | 6 + provider/plugin/httpapi.go | 169 ++++++++++++++ provider/plugin/httpapi_test.go | 210 +++++++++++++++++ provider/plugin/plugin.go | 349 +++++++++++++++++++++++++++++ provider/plugin/plugin_test.go | 172 ++++++++++++++ 11 files changed, 993 insertions(+), 1 deletion(-) create mode 100644 docs/img/plugin-provider.png create mode 100644 docs/tutorials/plugin-provider.md create mode 100644 provider/plugin/httpapi.go create mode 100644 provider/plugin/httpapi_test.go create mode 100644 provider/plugin/plugin.go create mode 100644 provider/plugin/plugin_test.go diff --git a/docs/img/plugin-provider.png b/docs/img/plugin-provider.png new file mode 100644 index 0000000000000000000000000000000000000000..11cdb217ac4d88e0e995a7fce2c21975f87d8a28 GIT binary patch literal 6733 zcmeI1i9eL@yT=D3qAW?qmzXGu7_#qVOF~kSH3nnf24i0$*MH`+hvRsjGRC?k_qR40clMn%XTG zi~}GZKC6mJST&#|MLjJb=OWz@?B`7|crq z2AjVRgUKetU~FzLYxU*9z)c$?En6KOm;lhUFiMJ3Fe;!Zz&9+y9tQuTVKCqcd{Jdl z{ChNu@}Hv==q###YFH~|*nDoH95}gWf7{5@Nauzu+SOUu(%RL^M%c&M4Kjeq`^W<6 zY~yK(^l^4_!OHq5@cglm1sb}H;z9l~@pM$+G19q-RCV>RK}rgX2#fG2(jk#Zc@Jw_ z*;{HF|F|7|QsA-k^mLO&p>Q~yFiuR^)dPbPm64G_iCjTlxgrECgs{FYo|ZmBE?C~b zo&3{}nhh51VejT?@9Kht{90PMdU-1F@IVv&>+A1zdfMCmdnOm`Kf(e5qM#9!sIUm? zU%tVu^3YXTJr8>uFf-&|QB?kq<$t;Mk9p)#(B%JVF@Hz;=PC$Qkxm}hA=?PP4{YslYvteEGkxCHk)fqcB4X+ZmX68yp5hObfIn zD@+8HCxw0DQw@W`k+4vse^`rn6Fslwu-~<^*1!tP#M8Iny3z(=a1GI=X z3d+as3@0efAPcZen*Ziy-j1$iq^73UPix|W(w5H01aH(O2V zPIQYNhjco~w(bKgltNLEI&?^k7NqAuay)rnLIryq3bW#*^1N{s=xh8Th&+~GoKU7u z024$O`V^#PK_Sy=Nzl@)K;BwmXA}NjCbDHtO*#T3lZgtL9Ggvk0Ht0RCaTN-`G3gL zLnG(dXSV9Iva;&Ojjgv?;VRZuU(?PJEy%pa<0Omm$E9zs`f_(H=K22+Lh`AXUz%ej zT4-t6y!aNetC`G$9c>SV;*$aKc}yez#HR&ztMyd|s2pv_kuA@7Tal2y0rJkr;sNa` zHIRZBPMKp+X{=8vM~kJzQ9105kLTIkC9m@N2$5(H58SDdw!4H~xx5)P=ZhtI>L2tijEjaiv}h1BFIzy?J_4 z6M@@ir8b{yeSk>$o-S8M{7b?iv4`;JBuA>;N`0@>1b)oSFw;}X(9~T$a<>%Sn#!Up zeeRi*$Mm)ADhAnEq`?Odo8BTbxpJSi$tEubkAzQ)L*)i77U;lnUs6i-xSwDmgX>UP zbLG)e^@MH3_7=e+Z45V|z?&KEMmzlxL66Q<4D2Yg`F2b4xT(7qL1}-nd@}NZw-7Dm zMwil24S70c)PLn8k(3$iJ?YziAt_luTO%dE7%$sAJ{7CvLhMLMXfxnY>`a#9}#xN=Jh3$4^7TU{{zlfqOFFnN+)cy_>CZa$~bskHh!-DaG9Vm4*pZ zpI;B(xK7}04-ao72NVWH}SXP{vw(@5gg20Tw~D?M0~=Z zA#!SBrQt|XzeqbtSU0QMdE`ag2F>__40#Z((;UhAl+38G8D~FMTZ#INPlSu}i(@vw za6P=}cu;9SPM-borNFFi;Ie5=w_TCFuH8ZLN3wCYjP(6aFznAznp)pnnSGGi)VdY? zNT)?`8vU8eZdmMCt(QxX>tIQvncv|qNhg6%XS9+(GoXhJw;$iJ>x$py%VpqiW-4i@ zc*ZyK{MwQ)T75`iCeeU$;SM~3Ur%>vpxAVy{t{Yi%u>LI^)o^gD$VLQ9?D&NL zP7}f9XXi#R!%=&$A1lW1y{{v;@y5bk>(?}H=INfrbv|M+4eEb?XY9k4?dXT$+j8Vk zcTyR8swyyQ?Xw9sgj??lLY+TH*7HS1RH z^+qoLMdr=rF}WSEPg%r2Pv-6BRu%lD2|ifUED6|M|GBqNjKj9#$Ka`gwd1eVqkeWu zO`H&4P7O{K8<_IKem-=A_rB4=&h$NsXC?jYGtl#Levc`N3S@|n!d}y z1_7g@fT|;(x}hqA;XVD~r>5-L^&hNP)rX!VwBfOG%QcCeex*gVfw?%ATa^AY&kgxf zoknZiR~O5>YBwaEhovpI*a}GtYNJu5TX#n<=mc!3GCqmtx|@zNuDG}}lVlP0&LFQg zU||L>8O`+8_4<)s^c{_*S&9v-0!iDB2dgVii*hLi1!PqPmRSm63`iH!H}Ec1`z1TitGGdg2gjqjiv!6}6J(yccJOOU|)prkCmArD6(6 zbOB?GiaSXC>KyDiq0^iDOhXTu_j+AZw9wLI{7H7iJ`5rEuJdPOIK7aA0uOe?xtd+> z*;tiB|NF##=RzO;JUUi^^O?2ig;zC$8F!m2liv)YwGy_v&Gb2B+uvGQWIo-h86%>G z3C6vZj)uYWlLRUn22avnU*TJ8{<6~#3u%u4Q^XsHe7n#KJ>&Jx0vmhj$l7C<<%^y_WeA6r87 z7c-wx;(u&>uzJt1FNlfx=G5{Fep)Ubnl^hA!FQd~ z$aywal0jygX}f)h*qN4|cPIDy^Z45+FZO+9_Ky^8=9Lcpk%bL{JFJc+(Pn+P7Qb+T zukxhB zDmM;_`I$6k)Y6QCc7J&qZ{H&b>*xSJosJf#gn&#Az?t{K+mnz!4fI8XJsd(TZ-85i zJNg`uz6kV9ksexz_Z9(`53OZOLi#4qukPnCLj*U(tQ=iFrRfamR{*+I8?W&~OjZU? zMwr&vVMt#+7lMcgd!_-wqO~UA$r$cR2o@h)g^4my^FD>}uPp`;;#nmJh|*~v0u-dv z_zMEMxU*o>&4`1-!c-DaKN(bqYQ3Dx#)tDs+Ds%{Ndjq%l0i1b|5zxU<9N|d~l4$`ddKjaC z14pE3CXm30W8p<0%e&g()`?OQI8lXi@_-X(_x)~G+C)vthJ*E}p&Iuo;fBiK!`;@! z!BTY6$k3f*OjBm+FTBsKqC$ttF&w|Q%q&MzB^)ECE3BI7Hb${stiB2z^jOk&2^vnf z5J!)_&ACf~V*vYfzP%9Y1b~tu@~&)azF!Q$n9j72dHrDjK4{Qp;|2H1J7h>N(c}?V zB?Hj3dI92YtbarCe~kzfvF$+g0%9#mds?8-Bz>h1@zs#>uz2N#kcn-N6e{FlX^K)X zer~J|XPI=h%YggnU_*~kw6X%I)2TvXcRJw>00LsoRE!usoA8anfsgW=-KtNp7JBR^ zRZgOq%)qLVprhZo4S<)@qAsAJpEvh-ZD2DC6BSksS2z478CvVLkh-Z;+`leMbMmig z7u}39`D@4TcV#G~`&hU&QZEl{atB)L$}NeL8qK|jY&43Gt^-mO`MtL#W>)JdN-*~? zWLNO3kXsr3P%h}d{DG7T$o)fEW*|95-+zzU4nUvp_887A$nt0rfTqTXFDV^k9%0Z@~g_d4Z*<#6lxE@I9%yC7I^Zk zY@fK(V7GHSSA~<5Ltk5)sw<%={a&TR^ZfTEO=h^UnXlidj#j@g1}4~e#3GW~Ewg)V zo_$KZWN>4h3@VVOKpy;J>Nl55958W}@at@tdy_U=Sm``6VCvO>NB4wl!*6fWs|=sX zC&XlQjQgvolN^%C&F=lNkLtZL&lBnBHv#QpE|4QQJs!(|QaxQ(C0O|U zqGs!f^BDjq6n$M9@WjO`(E&u2q?4F!ot%t9Nsd&z;xcN6;|B3-EgZsp1^%})L4NOT zfH3R)r8hdnIsw!h3=3iT$!~ac&fLr)iyRBBIc~)KW#d zbKg6I*5~D~7uO3l5k@N5R?t4&33u*l+4K=(l@1Xn5F*GDE~nM)iQqcN0c{3dg{|AT zf?%-SS&ZDVByuZq5NzQ4Q?-pOdh)-9OEMfpFyCKh0kA0Q0ItpOa$0*BaKLE8k=s|gUo3)h0o}JL*KPi z&TzDkw`$T!71w%a6|8JJYQ#m>Mtw}IdGk==A-~IqnKRY*EG5ycF<#rgpv-mMny*(I zXy%jReI7T~`)-)hPUj@(?o~e!rQr@Jk-Ix+(-O^Yd$6Z&Q_59M-?`%mPHI~b|LIR8WU_EB zO>H>^li@Df5B9;};ObSL7(tYg1&`e`z1`ZwcK%H1F89F_OX9&sm)_c=BW{+4pZqBB zs^u{=uPLk01J)NWF5k1NXjkn|V)kFEO!@#$Rbr9z+EL&M(%11)M$BDjyrAB7%Rtm} z$Y-gN@Dhh&7P@@Nyukl?oOZHTmvKPA?=3>Qbbid_*F7H9QZiuYWUT}~_T!sj)byNR zJ`W3t`C7IZUhjE9Z4hsj7^?i?-2H}Xou(TSM+s8<65&z(6W z#HE3zlqzuNea`7#HlFnttb1>b)*5n!*YtPPE$nPk)?a^KhK;~! z4W`LE-EyGc>Vbaid!>7n4?xNYAf-_*Za*5(Z;yk1t9LAQ1l-_}2B2nSN_7Q5lRSVR zQoAxX4J^QgWoKoDj(0H+KgO#9fG6ZrFr0@CeP|JR^b-pQNcM>ZdE=K>$)A^vyuhEv zPBKJrKf#Nx47fkUTB4xyqaVcDLplRywfO(W|H{UvBUt=xcwIHEg8}s47A= 300 && resp.StatusCode < 500 { + return backoff.Permanent(fmt.Errorf("status code < 500")) + } + return nil + }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries)) + + if err != nil { + return nil, fmt.Errorf("failed to connect to plugin api: %v", err) + } + + vary := resp.Header.Get(varyHeader) + contentType := resp.Header.Get(contentTypeHeader) + + if vary != contentTypeHeader { + return nil, fmt.Errorf("wrong vary value returned from server: %s", vary) + } + + if contentType != mediaTypeFormatAndVersion { + return nil, fmt.Errorf("wrong content type returned from server: %s", contentType) + } + + return &PluginProvider{ + client: client, + remoteServerURL: parsedURL, + }, nil +} + +// Records will make a GET call to remoteServerURL/records and return the results +func (p PluginProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { + u, err := url.JoinPath(p.remoteServerURL.String(), "records") + if err != nil { + recordsErrorsGauge.Inc() + log.Debugf("Failed to join path: %s", err.Error()) + return nil, err + } + req, err := http.NewRequest("GET", u, nil) + if err != nil { + recordsErrorsGauge.Inc() + log.Debugf("Failed to create request: %s", err.Error()) + return nil, err + } + req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) + resp, err := p.client.Do(req) + if err != nil { + recordsErrorsGauge.Inc() + log.Debugf("Failed to perform request: %s", err.Error()) + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + recordsErrorsGauge.Inc() + log.Debugf("Failed to get records with code %d", resp.StatusCode) + return nil, fmt.Errorf("failed to get records with code %d", resp.StatusCode) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + recordsErrorsGauge.Inc() + log.Debugf("Failed to read response body: %s", err.Error()) + return nil, err + } + + endpoints := []*endpoint.Endpoint{} + err = json.Unmarshal(b, &endpoints) + if err != nil { + recordsErrorsGauge.Inc() + log.Debugf("Failed to unmarshal response body: %s", err.Error()) + return nil, err + } + return endpoints, nil +} + +// ApplyChanges will make a POST to remoteServerURL/records with the changes +func (p PluginProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + u, err := url.JoinPath(p.remoteServerURL.String(), "records") + if err != nil { + applyChangesErrorsGauge.Inc() + log.Debugf("Failed to join path: %s", err.Error()) + return err + } + b, err := json.Marshal(changes) + if err != nil { + applyChangesErrorsGauge.Inc() + log.Debugf("Failed to marshal changes: %s", err.Error()) + return err + } + + req, err := http.NewRequest("POST", u, bytes.NewBuffer(b)) + if err != nil { + applyChangesErrorsGauge.Inc() + log.Debugf("Failed to create request: %s", err.Error()) + return err + } + resp, err := p.client.Do(req) + if err != nil { + applyChangesErrorsGauge.Inc() + log.Debugf("Failed to perform request: %s", err.Error()) + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + applyChangesErrorsGauge.Inc() + log.Debugf("Failed to apply changes with code %d", resp.StatusCode) + return fmt.Errorf("failed to apply changes with code %d", resp.StatusCode) + } + return nil +} + +// PropertyValuesEqual will call the provider doing a POST on `/propertyvaluesequal` which will return a boolean in the format +// `{propertyvaluesequal: true}` +// Errors in anything technically happening from the provider will return true so that no update is performed. +// Errors will also be logged and exposed as metrics so that it is possible to alert on them if needed. +func (p PluginProvider) PropertyValuesEqual(name string, previous string, current string) bool { + u, err := url.JoinPath(p.remoteServerURL.String(), "propertyvaluesequal") + if err != nil { + propertyValuesEqualErrorsGauge.Inc() + log.Debugf("Failed to join path: %s", err) + return true + } + b, err := json.Marshal(&PropertyValuesEqualRequest{ + Name: name, + Previous: previous, + Current: current, + }) + if err != nil { + propertyValuesEqualErrorsGauge.Inc() + log.Debugf("Failed to marshal request: %s", err) + return true + } + + req, err := http.NewRequest("POST", u, bytes.NewBuffer(b)) + if err != nil { + propertyValuesEqualErrorsGauge.Inc() + log.Debugf("Failed to create request: %s", err) + return true + } + req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) + resp, err := p.client.Do(req) + if err != nil { + propertyValuesEqualErrorsGauge.Inc() + log.Debugf("Failed to perform request: %s", err) + return true + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + propertyValuesEqualErrorsGauge.Inc() + log.Debugf("Failed to run PropertyValuesEqual with code %d", resp.StatusCode) + return true + } + + respoBody, err := io.ReadAll(resp.Body) + if err != nil { + propertyValuesEqualErrorsGauge.Inc() + log.Errorf("Failed to read body: %v", err) + return true + } + r := PropertyValuesEqualResponse{} + err = json.Unmarshal(respoBody, &r) + if err != nil { + propertyValuesEqualErrorsGauge.Inc() + log.Errorf("Failed to unmarshal body: %v", err) + return true + } + return r.Equals +} + +// AdjustEndpoints will call the provider doing a POST on `/adjustendpoints` which will return a list of modified endpoints +// based on a provider specific requirement. +// This method returns an empty slice in case there is a technical error on the provider's side so that no endpoints will be considered. +func (p PluginProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.Endpoint { + endpoints := []*endpoint.Endpoint{} + u, err := url.JoinPath(p.remoteServerURL.String(), "adjustendpoints") + if err != nil { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Failed to join path, %s", err) + return endpoints + } + b, err := json.Marshal(e) + if err != nil { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Failed to marshal endpoints, %s", err) + return endpoints + } + req, err := http.NewRequest("POST", u, bytes.NewBuffer(b)) + if err != nil { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Failed to create new HTTP request, %s", err) + return endpoints + } + req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) + resp, err := p.client.Do(req) + if err != nil { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Failed executing http request, %s", err) + return endpoints + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Failed to AdjustEndpoints with code %d", resp.StatusCode) + return endpoints + } + + b, err = io.ReadAll(resp.Body) + if err != nil { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Failed to read response body, %s", err) + return endpoints + } + + err = json.Unmarshal(b, &endpoints) + if err != nil { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Faile to unmarshal response body, %s", err) + return endpoints + } + return endpoints +} + +// GetDomainFilter is the default implementation of GetDomainFilter. +func (p PluginProvider) GetDomainFilter() endpoint.DomainFilterInterface { + return endpoint.DomainFilter{} +} diff --git a/provider/plugin/plugin_test.go b/provider/plugin/plugin_test.go new file mode 100644 index 0000000000..c2c32cff7f --- /dev/null +++ b/provider/plugin/plugin_test.go @@ -0,0 +1,172 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package plugin + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" + "sigs.k8s.io/external-dns/endpoint" +) + +func TestWronglyConfiguredNewPluginProvider(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(varyHeader, contentTypeHeader) + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.WriteHeader(400) + return + } + w.Write([]byte(`[{ + "dnsName" : "test.example.com" + }]`)) + })) + defer svr.Close() + + _, err := NewPluginProvider(svr.URL) + require.Error(t, err) +} + +func TestRecords(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(varyHeader, contentTypeHeader) + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.WriteHeader(200) + return + } + w.Write([]byte(`[{ + "dnsName" : "test.example.com" + }]`)) + })) + defer svr.Close() + + provider, err := NewPluginProvider(svr.URL) + require.Nil(t, err) + endpoints, err := provider.Records(context.TODO()) + require.Nil(t, err) + require.NotNil(t, endpoints) + require.Equal(t, []*endpoint.Endpoint{{ + DNSName: "test.example.com", + }}, endpoints) +} + +func TestApplyChanges(t *testing.T) { + successfulApplyChanges := true + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(varyHeader, contentTypeHeader) + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.WriteHeader(200) + return + } + if successfulApplyChanges { + w.WriteHeader(http.StatusNoContent) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + })) + defer svr.Close() + + provider, err := NewPluginProvider(svr.URL) + require.Nil(t, err) + err = provider.ApplyChanges(context.TODO(), nil) + require.Nil(t, err) + + successfulApplyChanges = false + + err = provider.ApplyChanges(context.TODO(), nil) + require.NotNil(t, err) +} + +func TestPropertyValuesEqual(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(varyHeader, contentTypeHeader) + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.WriteHeader(200) + return + } + j, _ := json.Marshal(&PropertyValuesEqualResponse{ + Equals: false, + }) + w.Write(j) + })) + defer svr.Close() + + provider, err := NewPluginProvider(svr.URL) + require.Nil(t, err) + b := provider.PropertyValuesEqual("name", "previous", "current") + require.Equal(t, false, b) +} + +func TestAdjustEndpoints(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(varyHeader, contentTypeHeader) + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.WriteHeader(200) + return + } + var endpoints []*endpoint.Endpoint + defer r.Body.Close() + b, err := io.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + err = json.Unmarshal(b, &endpoints) + if err != nil { + t.Fatal(err) + } + + for _, e := range endpoints { + e.RecordTTL = 0 + } + j, _ := json.Marshal(endpoints) + w.Write(j) + + })) + defer svr.Close() + + provider, err := NewPluginProvider(svr.URL) + require.Nil(t, err) + endpoints := []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordTTL: 10, + RecordType: "A", + Targets: endpoint.Targets{ + "", + }, + }, + } + adjustedEndpoints := provider.AdjustEndpoints(endpoints) + require.Equal(t, []*endpoint.Endpoint{{ + DNSName: "test.example.com", + RecordTTL: 0, + RecordType: "A", + Targets: endpoint.Targets{ + "", + }, + }}, adjustedEndpoints) + +} From dd170b9e0e67a0114a59ef0c8e3b40cd1fbff715 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sun, 18 Jun 2023 15:33:51 +0200 Subject: [PATCH 02/19] rename to webhook Signed-off-by: Raffaele Di Fazio --- ...ugin-provider.png => webhook-provider.png} | Bin ...plugin-provider.md => webhook-provider.md} | 20 +++++------ main.go | 10 +++--- pkg/apis/externaldns/types.go | 26 +++++++------- pkg/apis/externaldns/types_test.go | 12 +++---- provider/{plugin => webhook}/httpapi.go | 2 +- provider/{plugin => webhook}/httpapi_test.go | 32 +++++++++--------- .../{plugin/plugin.go => webhook/webhook.go} | 32 +++++++++--------- .../webhook_test.go} | 14 ++++---- 9 files changed, 74 insertions(+), 74 deletions(-) rename docs/img/{plugin-provider.png => webhook-provider.png} (100%) rename docs/tutorials/{plugin-provider.md => webhook-provider.md} (63%) rename provider/{plugin => webhook}/httpapi.go (99%) rename provider/{plugin => webhook}/httpapi_test.go (84%) rename provider/{plugin/plugin.go => webhook/webhook.go} (91%) rename provider/{plugin/plugin_test.go => webhook/webhook_test.go} (93%) diff --git a/docs/img/plugin-provider.png b/docs/img/webhook-provider.png similarity index 100% rename from docs/img/plugin-provider.png rename to docs/img/webhook-provider.png diff --git a/docs/tutorials/plugin-provider.md b/docs/tutorials/webhook-provider.md similarity index 63% rename from docs/tutorials/plugin-provider.md rename to docs/tutorials/webhook-provider.md index 3080376cd6..efa2a50651 100644 --- a/docs/tutorials/plugin-provider.md +++ b/docs/tutorials/webhook-provider.md @@ -1,12 +1,12 @@ -# Plugin provider +# Webhook provider -The "Plugin" provider allows to integrate ExternalDNS with DNS providers via an HTTP interface. -The Plugin provider implements the Provider interface. Instead of implementing code specific to a provider, it implements an HTTP client that sends request to an HTTP API. -The idea behind it is that providers can be implemented in a separate program: these programs expose an HTTP API that the Plugin provider can interact with. The ideal setup for providers is to run as sidecars in the same pod of the ExternalDNS container and listen on localhost only. This is not strictly a requirement, but we would not recommend other setups. +The "Webhook" provider allows to integrate ExternalDNS with DNS providers via an HTTP interface. +The Webhook provider implements the Provider interface. Instead of implementing code specific to a provider, it implements an HTTP client that sends request to an HTTP API. +The idea behind it is that providers can be implemented in a separate program: these programs expose an HTTP API that the Webhook provider can interact with. The ideal setup for providers is to run as sidecars in the same pod of the ExternalDNS container and listen on localhost only. This is not strictly a requirement, but we would not recommend other setups. ## Architectural diagram -![Plugin provider](../img/plugin-provider.png) +![Webhook provider](../img/webhook-provider.png) ## API guarantees @@ -32,13 +32,13 @@ Additionally, the server needs to respond to `GET` requests on `/` to negotiate To simplify the discovery of providers, we will accept pull requests that will add links to providers in the [README](../../README.md) file. This list will serve the only purpose of simplifying finding providers and will not constitute an official endorsment of any of the externally implemented providers unless otherwise specified. -## Run the AWS provider with the plugin provider. +## Run the AWS provider with the webhook provider. -To test the Plugin provider and provide a reference implementation, we added the functionality to run the AWS provider as a plugin. To run the AWS provider as a plugin, you need the following flags: +To test the Webhook provider and provide a reference implementation, we added the functionality to run the AWS provider as a webhook. To run the AWS provider as a webhook, you need the following flags: ```yaml -- --provider=plugin -- --run-aws-provider-as-plugin +- --provider=webhook +- --run-aws-provider-as-webhook ``` -What will happen behind the scenes is that the AWS provider will be be started as an HTTP server exposed only on localhost and the plugin provider will be configured to talk to it. This is the same setup that we recommend for other providers and a good way to test the Plugin provider other than to serve as a reference implementation. +What will happen behind the scenes is that the AWS provider will be be started as an HTTP server exposed only on localhost and the webhook provider will be configured to talk to it. This is the same setup that we recommend for other providers and a good way to test the Webhook provider other than to serve as a reference implementation. diff --git a/main.go b/main.go index 15d70bd2ba..ef58a2e4a3 100644 --- a/main.go +++ b/main.go @@ -62,7 +62,6 @@ import ( "sigs.k8s.io/external-dns/provider/ovh" "sigs.k8s.io/external-dns/provider/pdns" "sigs.k8s.io/external-dns/provider/pihole" - "sigs.k8s.io/external-dns/provider/plugin" "sigs.k8s.io/external-dns/provider/plural" "sigs.k8s.io/external-dns/provider/rcode0" "sigs.k8s.io/external-dns/provider/rdns" @@ -74,6 +73,7 @@ import ( "sigs.k8s.io/external-dns/provider/ultradns" "sigs.k8s.io/external-dns/provider/vinyldns" "sigs.k8s.io/external-dns/provider/vultr" + "sigs.k8s.io/external-dns/provider/webhook" "sigs.k8s.io/external-dns/registry" "sigs.k8s.io/external-dns/source" ) @@ -372,9 +372,9 @@ func main() { p, err = plural.NewPluralProvider(cfg.PluralCluster, cfg.PluralProvider) case "tencentcloud": p, err = tencentcloud.NewTencentCloudProvider(domainFilter, zoneIDFilter, cfg.TencentCloudConfigFile, cfg.TencentCloudZoneType, cfg.DryRun) - case "plugin": + case "webhook": startedChan := make(chan struct{}) - if cfg.RunAWSProviderAsPlugin { + if cfg.RunAWSProviderAsWebhook { awsProvider, awsErr := aws.NewAWSProvider(aws.AWSConfig{ DomainFilter: domainFilter, ZoneIDFilter: zoneIDFilter, @@ -393,10 +393,10 @@ func main() { if awsErr != nil { log.Fatal(awsErr) } - go plugin.StartHTTPApi(awsProvider, startedChan, cfg.PluginProviderReadTimeout, cfg.PluginProviderWriteTimeout, "127.0.0.1:8888") + go webhook.StartHTTPApi(awsProvider, startedChan, cfg.WebhookProviderReadTimeout, cfg.WebhookProviderWriteTimeout, "127.0.0.1:8888") <-startedChan } - p, err = plugin.NewPluginProvider(cfg.PluginProviderURL) + p, err = webhook.NewWebhookProvider(cfg.WebhookProviderURL) default: log.Fatalf("unknown dns provider: %s", cfg.Provider) } diff --git a/pkg/apis/externaldns/types.go b/pkg/apis/externaldns/types.go index 65080caba9..a427021272 100644 --- a/pkg/apis/externaldns/types.go +++ b/pkg/apis/externaldns/types.go @@ -205,10 +205,10 @@ type Config struct { PiholeTLSInsecureSkipVerify bool PluralCluster string PluralProvider string - PluginProviderURL string - RunAWSProviderAsPlugin bool - PluginProviderReadTimeout time.Duration - PluginProviderWriteTimeout time.Duration + WebhookProviderURL string + RunAWSProviderAsWebhook bool + WebhookProviderReadTimeout time.Duration + WebhookProviderWriteTimeout time.Duration } var defaultConfig = &Config{ @@ -354,9 +354,9 @@ var defaultConfig = &Config{ PiholeTLSInsecureSkipVerify: false, PluralCluster: "", PluralProvider: "", - PluginProviderURL: "http://localhost:8888", - PluginProviderReadTimeout: 5 * time.Second, - PluginProviderWriteTimeout: 10 * time.Second, + WebhookProviderURL: "http://localhost:8888", + WebhookProviderReadTimeout: 5 * time.Second, + WebhookProviderWriteTimeout: 10 * time.Second, } // NewConfig returns new Config object @@ -448,7 +448,7 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("exclude-target-net", "Exclude target nets (optional)").StringsVar(&cfg.ExcludeTargetNets) // Flags related to providers - providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "infoblox", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr", "plugin"} + providers := []string{"akamai", "alibabacloud", "aws", "aws-sd", "azure", "azure-dns", "azure-private-dns", "bluecat", "civo", "cloudflare", "coredns", "designate", "digitalocean", "dnsimple", "dyn", "exoscale", "gandi", "godaddy", "google", "ibmcloud", "infoblox", "inmemory", "linode", "ns1", "oci", "ovh", "pdns", "pihole", "plural", "rcodezero", "rdns", "rfc2136", "safedns", "scaleway", "skydns", "tencentcloud", "transip", "ultradns", "vinyldns", "vultr", "webhook"} app.Flag("provider", "The DNS provider where the DNS records will be created (required, options: "+strings.Join(providers, ", ")+")").Required().PlaceHolder("provider").EnumVar(&cfg.Provider, providers...) app.Flag("domain-filter", "Limit possible target zones by a domain suffix; specify multiple times for multiple domains (optional)").Default("").StringsVar(&cfg.DomainFilter) app.Flag("exclude-domains", "Exclude subdomains (optional)").Default("").StringsVar(&cfg.ExcludeDomains) @@ -600,11 +600,11 @@ func (cfg *Config) ParseFlags(args []string) error { app.Flag("metrics-address", "Specify where to serve the metrics and health check endpoint (default: :7979)").Default(defaultConfig.MetricsAddress).StringVar(&cfg.MetricsAddress) app.Flag("log-level", "Set the level of logging. (default: info, options: panic, debug, info, warning, error, fatal").Default(defaultConfig.LogLevel).EnumVar(&cfg.LogLevel, allLogLevelsAsStrings()...) - // Plugin provider - app.Flag("plugin-provider-url", "[EXPERIMENTAL] The URL of the remote endpoint to call for the plugin provider (default: http://localhost:8888)").Default(defaultConfig.PluginProviderURL).StringVar(&cfg.PluginProviderURL) - app.Flag("run-aws-provider-as-plugin", "[EXPERIMENTAL] When enabled, the AWS provider will be run as a plugin (default: false). To be used together with 'plugin' as provider.").BoolVar(&cfg.RunAWSProviderAsPlugin) - app.Flag("plugin-provider-read-timeout", "[EXPERIMENTAL] The read timeout for the plugin provider in duration format (default: 5s)").Default(defaultConfig.PluginProviderReadTimeout.String()).DurationVar(&cfg.PluginProviderReadTimeout) - app.Flag("plugin-provider-write-timeout", "[EXPERIMENTAL] The write timeout for the plugin provider in duration format (default: 10s)").Default(defaultConfig.PluginProviderWriteTimeout.String()).DurationVar(&cfg.PluginProviderWriteTimeout) + // Webhook provider + app.Flag("webhook-provider-url", "[EXPERIMENTAL] The URL of the remote endpoint to call for the webhook provider (default: http://localhost:8888)").Default(defaultConfig.WebhookProviderURL).StringVar(&cfg.WebhookProviderURL) + app.Flag("run-aws-provider-as-webhook", "[EXPERIMENTAL] When enabled, the AWS provider will be run as a webhook (default: false). To be used together with 'webhook' as provider.").BoolVar(&cfg.RunAWSProviderAsWebhook) + app.Flag("webhook-provider-read-timeout", "[EXPERIMENTAL] The read timeout for the webhook provider in duration format (default: 5s)").Default(defaultConfig.WebhookProviderReadTimeout.String()).DurationVar(&cfg.WebhookProviderReadTimeout) + app.Flag("webhook-provider-write-timeout", "[EXPERIMENTAL] The write timeout for the webhook provider in duration format (default: 10s)").Default(defaultConfig.WebhookProviderWriteTimeout.String()).DurationVar(&cfg.WebhookProviderWriteTimeout) _, err := app.Parse(args) if err != nil { diff --git a/pkg/apis/externaldns/types_test.go b/pkg/apis/externaldns/types_test.go index a450925204..8990f04e2b 100644 --- a/pkg/apis/externaldns/types_test.go +++ b/pkg/apis/externaldns/types_test.go @@ -129,9 +129,9 @@ var ( IBMCloudConfigFile: "/etc/kubernetes/ibmcloud.json", TencentCloudConfigFile: "/etc/kubernetes/tencent-cloud.json", TencentCloudZoneType: "", - PluginProviderURL: "http://localhost:8888", - PluginProviderReadTimeout: 5 * time.Second, - PluginProviderWriteTimeout: 10 * time.Second, + WebhookProviderURL: "http://localhost:8888", + WebhookProviderReadTimeout: 5 * time.Second, + WebhookProviderWriteTimeout: 10 * time.Second, } overriddenConfig = &Config{ @@ -242,9 +242,9 @@ var ( IBMCloudConfigFile: "ibmcloud.json", TencentCloudConfigFile: "tencent-cloud.json", TencentCloudZoneType: "private", - PluginProviderURL: "http://localhost:8888", - PluginProviderReadTimeout: 5 * time.Second, - PluginProviderWriteTimeout: 10 * time.Second, + WebhookProviderURL: "http://localhost:8888", + WebhookProviderReadTimeout: 5 * time.Second, + WebhookProviderWriteTimeout: 10 * time.Second, } ) diff --git a/provider/plugin/httpapi.go b/provider/webhook/httpapi.go similarity index 99% rename from provider/plugin/httpapi.go rename to provider/webhook/httpapi.go index 0fb6b62a55..b2633b57be 100644 --- a/provider/plugin/httpapi.go +++ b/provider/webhook/httpapi.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugin +package webhook import ( "context" diff --git a/provider/plugin/httpapi_test.go b/provider/webhook/httpapi_test.go similarity index 84% rename from provider/plugin/httpapi_test.go rename to provider/webhook/httpapi_test.go index 8b79a5213a..b47d77dfb5 100644 --- a/provider/plugin/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugin +package webhook import ( "bytes" @@ -30,25 +30,25 @@ import ( "sigs.k8s.io/external-dns/plan" ) -type FakePluginProvider struct{} +type FakeWebhookProvider struct{} -func (p FakePluginProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { +func (p FakeWebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { return []*endpoint.Endpoint{}, nil } -func (p FakePluginProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { +func (p FakeWebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { return nil } -func (p FakePluginProvider) PropertyValuesEqual(name string, previous string, current string) bool { +func (p FakeWebhookProvider) PropertyValuesEqual(name string, previous string, current string) bool { return false } -func (p FakePluginProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { +func (p FakeWebhookProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { return endpoints } -func (p FakePluginProvider) GetDomainFilter() endpoint.DomainFilterInterface { +func (p FakeWebhookProvider) GetDomainFilter() endpoint.DomainFilterInterface { return endpoint.DomainFilter{} } @@ -57,7 +57,7 @@ func TestRecordsHandlerRecords(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.recordsHandler(w, req) res := w.Result() @@ -69,7 +69,7 @@ func TestRecordsHandlerApplyChangesWithBadRequest(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.recordsHandler(w, req) res := w.Result() @@ -95,7 +95,7 @@ func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.recordsHandler(w, req) res := w.Result() @@ -107,7 +107,7 @@ func TestPropertyValuesEqualHandlerWithInvalidRequests(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.propertyValuesEqualHandler(w, req) res := w.Result() @@ -135,7 +135,7 @@ func TestPropertyValuesEqualWithValidRequest(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.propertyValuesEqualHandler(w, req) res := w.Result() @@ -148,7 +148,7 @@ func TestAdjustEndpointsHandlerWithInvalidRequest(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.adjustEndpointsHandler(w, req) res := w.Result() @@ -179,7 +179,7 @@ func TestAdjustEndpointsWithValidRequest(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.adjustEndpointsHandler(w, req) res := w.Result() @@ -191,7 +191,7 @@ func TestNegotiate(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/", nil) w := httptest.NewRecorder() providerAPIServer := &ProviderAPIServer{ - provider: &FakePluginProvider{}, + provider: &FakeWebhookProvider{}, } providerAPIServer.negotiate(w, req) res := w.Result() @@ -202,7 +202,7 @@ func TestNegotiate(t *testing.T) { func TestStartHTTPApi(t *testing.T) { startedChan := make(chan struct{}) - go StartHTTPApi(FakePluginProvider{}, startedChan, 5*time.Second, 10*time.Second, "127.0.0.1:8887") + go StartHTTPApi(FakeWebhookProvider{}, startedChan, 5*time.Second, 10*time.Second, "127.0.0.1:8887") <-startedChan resp, err := http.Get("http://127.0.0.1:8887") require.NoError(t, err) diff --git a/provider/plugin/plugin.go b/provider/webhook/webhook.go similarity index 91% rename from provider/plugin/plugin.go rename to provider/webhook/webhook.go index f0c665f380..d154b5d993 100644 --- a/provider/plugin/plugin.go +++ b/provider/webhook/webhook.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugin +package webhook import ( "bytes" @@ -34,7 +34,7 @@ import ( ) const ( - mediaTypeFormatAndVersion = "application/external.dns.plugin+json;version=1" + mediaTypeFormatAndVersion = "application/external.dns.webhook+json;version=1" contentTypeHeader = "Content-Type" acceptHeader = "Accept" varyHeader = "Vary" @@ -45,7 +45,7 @@ var ( recordsErrorsGauge = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "external_dns", - Subsystem: "plugin_provider", + Subsystem: "webhook_provider", Name: "records_errors", Help: "Errors with Records method", }, @@ -53,7 +53,7 @@ var ( applyChangesErrorsGauge = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "external_dns", - Subsystem: "plugin_provider", + Subsystem: "webhook_provider", Name: "applychanges_errors", Help: "Errors with ApplyChanges method", }, @@ -61,7 +61,7 @@ var ( propertyValuesEqualErrorsGauge = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "external_dns", - Subsystem: "plugin_provider", + Subsystem: "webhook_provider", Name: "propertyvaluesequal_errors", Help: "Errors with PropertyValuesEqual method", }, @@ -69,14 +69,14 @@ var ( adjustEndpointsErrorsGauge = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "external_dns", - Subsystem: "plugin_provider", + Subsystem: "webhook_provider", Name: "adjustendpointsgauge_errors", Help: "Errors with AdjustEndpoints method", }, ) ) -type PluginProvider struct { +type WebhookProvider struct { client *http.Client remoteServerURL *url.URL } @@ -98,7 +98,7 @@ func init() { prometheus.MustRegister(adjustEndpointsErrorsGauge) } -func NewPluginProvider(u string) (*PluginProvider, error) { +func NewWebhookProvider(u string) (*WebhookProvider, error) { parsedURL, err := url.Parse(u) if err != nil { return nil, err @@ -116,7 +116,7 @@ func NewPluginProvider(u string) (*PluginProvider, error) { err = backoff.Retry(func() error { resp, err = client.Do(req) if err != nil { - log.Debugf("Failed to connect to plugin api: %v", err) + log.Debugf("Failed to connect to webhook api: %v", err) return err } // we currently only use 200 as success, but considering okay all 2XX for future usage @@ -127,7 +127,7 @@ func NewPluginProvider(u string) (*PluginProvider, error) { }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries)) if err != nil { - return nil, fmt.Errorf("failed to connect to plugin api: %v", err) + return nil, fmt.Errorf("failed to connect to webhook api: %v", err) } vary := resp.Header.Get(varyHeader) @@ -141,14 +141,14 @@ func NewPluginProvider(u string) (*PluginProvider, error) { return nil, fmt.Errorf("wrong content type returned from server: %s", contentType) } - return &PluginProvider{ + return &WebhookProvider{ client: client, remoteServerURL: parsedURL, }, nil } // Records will make a GET call to remoteServerURL/records and return the results -func (p PluginProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { +func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { u, err := url.JoinPath(p.remoteServerURL.String(), "records") if err != nil { recordsErrorsGauge.Inc() @@ -194,7 +194,7 @@ func (p PluginProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, erro } // ApplyChanges will make a POST to remoteServerURL/records with the changes -func (p PluginProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { +func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { u, err := url.JoinPath(p.remoteServerURL.String(), "records") if err != nil { applyChangesErrorsGauge.Inc() @@ -234,7 +234,7 @@ func (p PluginProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) // `{propertyvaluesequal: true}` // Errors in anything technically happening from the provider will return true so that no update is performed. // Errors will also be logged and exposed as metrics so that it is possible to alert on them if needed. -func (p PluginProvider) PropertyValuesEqual(name string, previous string, current string) bool { +func (p WebhookProvider) PropertyValuesEqual(name string, previous string, current string) bool { u, err := url.JoinPath(p.remoteServerURL.String(), "propertyvaluesequal") if err != nil { propertyValuesEqualErrorsGauge.Inc() @@ -292,7 +292,7 @@ func (p PluginProvider) PropertyValuesEqual(name string, previous string, curren // AdjustEndpoints will call the provider doing a POST on `/adjustendpoints` which will return a list of modified endpoints // based on a provider specific requirement. // This method returns an empty slice in case there is a technical error on the provider's side so that no endpoints will be considered. -func (p PluginProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.Endpoint { +func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.Endpoint { endpoints := []*endpoint.Endpoint{} u, err := url.JoinPath(p.remoteServerURL.String(), "adjustendpoints") if err != nil { @@ -344,6 +344,6 @@ func (p PluginProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.Endp } // GetDomainFilter is the default implementation of GetDomainFilter. -func (p PluginProvider) GetDomainFilter() endpoint.DomainFilterInterface { +func (p WebhookProvider) GetDomainFilter() endpoint.DomainFilterInterface { return endpoint.DomainFilter{} } diff --git a/provider/plugin/plugin_test.go b/provider/webhook/webhook_test.go similarity index 93% rename from provider/plugin/plugin_test.go rename to provider/webhook/webhook_test.go index c2c32cff7f..f51398341c 100644 --- a/provider/plugin/plugin_test.go +++ b/provider/webhook/webhook_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package plugin +package webhook import ( "context" @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" ) -func TestWronglyConfiguredNewPluginProvider(t *testing.T) { +func TestWronglyConfiguredNewWebhookProvider(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { w.Header().Set(varyHeader, contentTypeHeader) @@ -42,7 +42,7 @@ func TestWronglyConfiguredNewPluginProvider(t *testing.T) { })) defer svr.Close() - _, err := NewPluginProvider(svr.URL) + _, err := NewWebhookProvider(svr.URL) require.Error(t, err) } @@ -60,7 +60,7 @@ func TestRecords(t *testing.T) { })) defer svr.Close() - provider, err := NewPluginProvider(svr.URL) + provider, err := NewWebhookProvider(svr.URL) require.Nil(t, err) endpoints, err := provider.Records(context.TODO()) require.Nil(t, err) @@ -87,7 +87,7 @@ func TestApplyChanges(t *testing.T) { })) defer svr.Close() - provider, err := NewPluginProvider(svr.URL) + provider, err := NewWebhookProvider(svr.URL) require.Nil(t, err) err = provider.ApplyChanges(context.TODO(), nil) require.Nil(t, err) @@ -113,7 +113,7 @@ func TestPropertyValuesEqual(t *testing.T) { })) defer svr.Close() - provider, err := NewPluginProvider(svr.URL) + provider, err := NewWebhookProvider(svr.URL) require.Nil(t, err) b := provider.PropertyValuesEqual("name", "previous", "current") require.Equal(t, false, b) @@ -147,7 +147,7 @@ func TestAdjustEndpoints(t *testing.T) { })) defer svr.Close() - provider, err := NewPluginProvider(svr.URL) + provider, err := NewWebhookProvider(svr.URL) require.Nil(t, err) endpoints := []*endpoint.Endpoint{ { From e9430b157b7fcd3f85ef64538032a9db4f18a6c2 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sun, 18 Jun 2023 16:35:10 +0200 Subject: [PATCH 03/19] json encoder changes Signed-off-by: Raffaele Di Fazio --- provider/webhook/httpapi.go | 19 +++++----- provider/webhook/webhook.go | 72 ++++++++++++++----------------------- 2 files changed, 36 insertions(+), 55 deletions(-) diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index b2633b57be..3c76d54e4a 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -89,16 +89,16 @@ func (p *ProviderAPIServer) propertyValuesEqualHandler(w http.ResponseWriter, re w.WriteHeader(http.StatusBadRequest) return } + + w.Header().Set("Content-Type", "application/json") b := p.provider.PropertyValuesEqual(pve.Name, pve.Previous, pve.Current) r := PropertyValuesEqualsResponse{ Equals: b, } - out, err := json.Marshal(&r) - if err != nil { - log.Error(err) + if err := json.NewEncoder(w).Encode(&r); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - w.Header().Set("Content-Type", "application/json") - w.Write(out) } func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *http.Request) { @@ -113,13 +113,12 @@ func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *h w.WriteHeader(http.StatusBadRequest) return } + w.Header().Set("Content-Type", "application/json") pve = p.provider.AdjustEndpoints(pve) - out, err := json.Marshal(&pve) - if err != nil { - log.Error(err) + if err := json.NewEncoder(w).Encode(&pve); err != nil { + w.WriteHeader(http.StatusInternalServerError) + return } - w.Header().Set("Content-Type", "application/json") - w.Write(out) } func (p *ProviderAPIServer) negotiate(w http.ResponseWriter, req *http.Request) { diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index d154b5d993..42d23875d4 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -21,7 +21,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/url" @@ -176,18 +175,10 @@ func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err return nil, fmt.Errorf("failed to get records with code %d", resp.StatusCode) } - b, err := io.ReadAll(resp.Body) - if err != nil { - recordsErrorsGauge.Inc() - log.Debugf("Failed to read response body: %s", err.Error()) - return nil, err - } - endpoints := []*endpoint.Endpoint{} - err = json.Unmarshal(b, &endpoints) - if err != nil { + if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { recordsErrorsGauge.Inc() - log.Debugf("Failed to unmarshal response body: %s", err.Error()) + log.Debugf("Failed to decode response body: %s", err.Error()) return nil, err } return endpoints, nil @@ -201,14 +192,15 @@ func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes log.Debugf("Failed to join path: %s", err.Error()) return err } - b, err := json.Marshal(changes) - if err != nil { + + b := new(bytes.Buffer) + if err := json.NewEncoder(b).Encode(changes); err != nil { applyChangesErrorsGauge.Inc() - log.Debugf("Failed to marshal changes: %s", err.Error()) + log.Debugf("Failed to encode changes: %s", err.Error()) return err } - req, err := http.NewRequest("POST", u, bytes.NewBuffer(b)) + req, err := http.NewRequest("POST", u, b) if err != nil { applyChangesErrorsGauge.Inc() log.Debugf("Failed to create request: %s", err.Error()) @@ -241,18 +233,19 @@ func (p WebhookProvider) PropertyValuesEqual(name string, previous string, curre log.Debugf("Failed to join path: %s", err) return true } - b, err := json.Marshal(&PropertyValuesEqualRequest{ + + b := new(bytes.Buffer) + if err := json.NewEncoder(b).Encode(&PropertyValuesEqualRequest{ Name: name, Previous: previous, Current: current, - }) - if err != nil { - propertyValuesEqualErrorsGauge.Inc() - log.Debugf("Failed to marshal request: %s", err) + }); err != nil { + adjustEndpointsErrorsGauge.Inc() + log.Debugf("Failed to encode, %s", err) return true } - req, err := http.NewRequest("POST", u, bytes.NewBuffer(b)) + req, err := http.NewRequest("POST", u, b) if err != nil { propertyValuesEqualErrorsGauge.Inc() log.Debugf("Failed to create request: %s", err) @@ -273,19 +266,13 @@ func (p WebhookProvider) PropertyValuesEqual(name string, previous string, curre return true } - respoBody, err := io.ReadAll(resp.Body) - if err != nil { - propertyValuesEqualErrorsGauge.Inc() - log.Errorf("Failed to read body: %v", err) - return true - } r := PropertyValuesEqualResponse{} - err = json.Unmarshal(respoBody, &r) - if err != nil { - propertyValuesEqualErrorsGauge.Inc() - log.Errorf("Failed to unmarshal body: %v", err) + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + recordsErrorsGauge.Inc() + log.Debugf("Failed to decode response body: %s", err.Error()) return true } + return r.Equals } @@ -300,13 +287,15 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.End log.Debugf("Failed to join path, %s", err) return endpoints } - b, err := json.Marshal(e) - if err != nil { + + b := new(bytes.Buffer) + if err := json.NewEncoder(b).Encode(e); err != nil { adjustEndpointsErrorsGauge.Inc() - log.Debugf("Failed to marshal endpoints, %s", err) + log.Debugf("Failed to encode endpoints, %s", err) return endpoints } - req, err := http.NewRequest("POST", u, bytes.NewBuffer(b)) + + req, err := http.NewRequest("POST", u, b) if err != nil { adjustEndpointsErrorsGauge.Inc() log.Debugf("Failed to create new HTTP request, %s", err) @@ -327,19 +316,12 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.End return endpoints } - b, err = io.ReadAll(resp.Body) - if err != nil { - adjustEndpointsErrorsGauge.Inc() - log.Debugf("Failed to read response body, %s", err) + if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { + recordsErrorsGauge.Inc() + log.Debugf("Failed to decode response body: %s", err.Error()) return endpoints } - err = json.Unmarshal(b, &endpoints) - if err != nil { - adjustEndpointsErrorsGauge.Inc() - log.Debugf("Faile to unmarshal response body, %s", err) - return endpoints - } return endpoints } From 1bf2f5586f64cd7cf7f26b898210994ba1e0a3a2 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Mon, 19 Jun 2023 11:00:50 +0200 Subject: [PATCH 04/19] addressing review comments Signed-off-by: Raffaele Di Fazio --- provider/webhook/webhook.go | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index 42d23875d4..efbe50906b 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -148,12 +148,7 @@ func NewWebhookProvider(u string) (*WebhookProvider, error) { // Records will make a GET call to remoteServerURL/records and return the results func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - u, err := url.JoinPath(p.remoteServerURL.String(), "records") - if err != nil { - recordsErrorsGauge.Inc() - log.Debugf("Failed to join path: %s", err.Error()) - return nil, err - } + u := p.remoteServerURL.JoinPath("records").String() req, err := http.NewRequest("GET", u, nil) if err != nil { recordsErrorsGauge.Inc() @@ -186,12 +181,7 @@ func (p WebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, err // ApplyChanges will make a POST to remoteServerURL/records with the changes func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { - u, err := url.JoinPath(p.remoteServerURL.String(), "records") - if err != nil { - applyChangesErrorsGauge.Inc() - log.Debugf("Failed to join path: %s", err.Error()) - return err - } + u := p.remoteServerURL.JoinPath("records").String() b := new(bytes.Buffer) if err := json.NewEncoder(b).Encode(changes); err != nil { @@ -227,12 +217,7 @@ func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes // Errors in anything technically happening from the provider will return true so that no update is performed. // Errors will also be logged and exposed as metrics so that it is possible to alert on them if needed. func (p WebhookProvider) PropertyValuesEqual(name string, previous string, current string) bool { - u, err := url.JoinPath(p.remoteServerURL.String(), "propertyvaluesequal") - if err != nil { - propertyValuesEqualErrorsGauge.Inc() - log.Debugf("Failed to join path: %s", err) - return true - } + u := p.remoteServerURL.JoinPath("records").String() b := new(bytes.Buffer) if err := json.NewEncoder(b).Encode(&PropertyValuesEqualRequest{ From 41fda59ea24d845744b2bfa9aaaacc30236c29f7 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Tue, 20 Jun 2023 15:40:39 +0200 Subject: [PATCH 05/19] changes according to ionos review Signed-off-by: Raffaele Di Fazio --- docs/tutorials/webhook-provider.md | 2 +- provider/webhook/httpapi.go | 13 ++------- provider/webhook/httpapi_test.go | 16 +---------- provider/webhook/webhook.go | 45 ++++++------------------------ provider/webhook/webhook_test.go | 18 ------------ 5 files changed, 14 insertions(+), 80 deletions(-) diff --git a/docs/tutorials/webhook-provider.md b/docs/tutorials/webhook-provider.md index efa2a50651..1aa20a57b1 100644 --- a/docs/tutorials/webhook-provider.md +++ b/docs/tutorials/webhook-provider.md @@ -23,7 +23,7 @@ The following table represents the methods to implement mapped to their HTTP met | PropertyValuesEqual | POST | /propertyvaluesequal | | AdjustEndpoints | POST | /adjustendpoints | -Additionally, the server needs to respond to `GET` requests on `/` to negotiate versions by content type as described in [this document](http://opensource.zalando.com/restful-api-guidelines/#114). The server needs to respond to those requests by reading the `Accept` header and responding with a corresponding `Vary` header including the value `Content-Type` and a `Content-Type` header specifying the supported media type format and version. +The server needs to respond to those requests by reading the `Accept` header and responding with a corresponding `Content-Type` header specifying the supported media type format and version. **NOTE**: only `5xx` responses will be retried and only `200` will be considered as successful. All status codes different from those will be considered a failure on ExternalDNS' side. diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index 3c76d54e4a..b89aee9058 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -53,7 +53,7 @@ func (p *ProviderAPIServer) recordsHandler(w http.ResponseWriter, req *http.Requ w.WriteHeader(http.StatusInternalServerError) return } - w.Header().Set("Content-Type", "application/json") + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(records) return @@ -90,7 +90,7 @@ func (p *ProviderAPIServer) propertyValuesEqualHandler(w http.ResponseWriter, re return } - w.Header().Set("Content-Type", "application/json") + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) b := p.provider.PropertyValuesEqual(pve.Name, pve.Previous, pve.Current) r := PropertyValuesEqualsResponse{ Equals: b, @@ -113,7 +113,7 @@ func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *h w.WriteHeader(http.StatusBadRequest) return } - w.Header().Set("Content-Type", "application/json") + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) pve = p.provider.AdjustEndpoints(pve) if err := json.NewEncoder(w).Encode(&pve); err != nil { w.WriteHeader(http.StatusInternalServerError) @@ -121,12 +121,6 @@ func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *h } } -func (p *ProviderAPIServer) negotiate(w http.ResponseWriter, req *http.Request) { - w.Header().Set(varyHeader, contentTypeHeader) - w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - w.WriteHeader(http.StatusOK) -} - // StartHTTPApi starts a HTTP server given any provider. // the function takes an optional channel as input which is used to signal that the server has started. // The server will listen on port 8888. @@ -141,7 +135,6 @@ func StartHTTPApi(provider provider.Provider, startedChan chan struct{}, readTim } m := http.NewServeMux() - m.HandleFunc("/", p.negotiate) m.HandleFunc("/records", p.recordsHandler) m.HandleFunc("/propertyvaluesequal", p.propertyValuesEqualHandler) m.HandleFunc("/adjustendpoints", p.adjustEndpointsHandler) diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index b47d77dfb5..8c9107c4e3 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -187,24 +187,10 @@ func TestAdjustEndpointsWithValidRequest(t *testing.T) { require.NotNil(t, res.Body) } -func TestNegotiate(t *testing.T) { - req := httptest.NewRequest(http.MethodGet, "/", nil) - w := httptest.NewRecorder() - providerAPIServer := &ProviderAPIServer{ - provider: &FakeWebhookProvider{}, - } - providerAPIServer.negotiate(w, req) - res := w.Result() - require.Equal(t, contentTypeHeader, res.Header.Get(varyHeader)) - require.Equal(t, mediaTypeFormatAndVersion, res.Header.Get(contentTypeHeader)) - require.Equal(t, http.StatusOK, res.StatusCode) -} - func TestStartHTTPApi(t *testing.T) { startedChan := make(chan struct{}) go StartHTTPApi(FakeWebhookProvider{}, startedChan, 5*time.Second, 10*time.Second, "127.0.0.1:8887") <-startedChan - resp, err := http.Get("http://127.0.0.1:8887") + _, err := http.Get("http://127.0.0.1:8887") require.NoError(t, err) - require.Equal(t, mediaTypeFormatAndVersion, resp.Header.Get(contentTypeHeader)) } diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index efbe50906b..2a9c10799f 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -27,7 +27,6 @@ import ( "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" - backoff "github.com/cenkalti/backoff/v4" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" ) @@ -103,42 +102,7 @@ func NewWebhookProvider(u string) (*WebhookProvider, error) { return nil, err } - // negotiate API information - req, err := http.NewRequest("GET", u, nil) - if err != nil { - return nil, err - } - req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) - client := &http.Client{} - var resp *http.Response - err = backoff.Retry(func() error { - resp, err = client.Do(req) - if err != nil { - log.Debugf("Failed to connect to webhook api: %v", err) - return err - } - // we currently only use 200 as success, but considering okay all 2XX for future usage - if resp.StatusCode >= 300 && resp.StatusCode < 500 { - return backoff.Permanent(fmt.Errorf("status code < 500")) - } - return nil - }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries)) - - if err != nil { - return nil, fmt.Errorf("failed to connect to webhook api: %v", err) - } - - vary := resp.Header.Get(varyHeader) - contentType := resp.Header.Get(contentTypeHeader) - - if vary != contentTypeHeader { - return nil, fmt.Errorf("wrong vary value returned from server: %s", vary) - } - - if contentType != mediaTypeFormatAndVersion { - return nil, fmt.Errorf("wrong content type returned from server: %s", contentType) - } return &WebhookProvider{ client: client, @@ -196,6 +160,9 @@ func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes log.Debugf("Failed to create request: %s", err.Error()) return err } + + req.Header.Set(contentTypeHeader, mediaTypeFormatAndVersion) + resp, err := p.client.Do(req) if err != nil { applyChangesErrorsGauge.Inc() @@ -236,7 +203,10 @@ func (p WebhookProvider) PropertyValuesEqual(name string, previous string, curre log.Debugf("Failed to create request: %s", err) return true } + + req.Header.Set(contentTypeHeader, mediaTypeFormatAndVersion) req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) + resp, err := p.client.Do(req) if err != nil { propertyValuesEqualErrorsGauge.Inc() @@ -286,7 +256,10 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.End log.Debugf("Failed to create new HTTP request, %s", err) return endpoints } + + req.Header.Set(contentTypeHeader, mediaTypeFormatAndVersion) req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) + resp, err := p.client.Do(req) if err != nil { adjustEndpointsErrorsGauge.Inc() diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index f51398341c..c903307dfb 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -28,24 +28,6 @@ import ( "sigs.k8s.io/external-dns/endpoint" ) -func TestWronglyConfiguredNewWebhookProvider(t *testing.T) { - svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - w.Header().Set(varyHeader, contentTypeHeader) - w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - w.WriteHeader(400) - return - } - w.Write([]byte(`[{ - "dnsName" : "test.example.com" - }]`)) - })) - defer svr.Close() - - _, err := NewWebhookProvider(svr.URL) - require.Error(t, err) -} - func TestRecords(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { From 376f9477ffd94ca37e6e7df6a5e52670723fbe2f Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sat, 12 Aug 2023 16:57:56 +0200 Subject: [PATCH 06/19] fix to accomodate changes in master Signed-off-by: Raffaele Di Fazio --- go.mod | 1 - go.sum | 2 -- main.go | 5 +---- provider/webhook/httpapi_test.go | 2 +- provider/webhook/webhook.go | 2 +- 5 files changed, 3 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index 513a23c291..9c97eac090 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,6 @@ require ( github.com/ans-group/sdk-go v1.16.6 github.com/aws/aws-sdk-go v1.44.311 github.com/bodgit/tsig v1.2.2 - github.com/cenkalti/backoff/v4 v4.2.0 github.com/civo/civogo v0.3.42 github.com/cloudflare/cloudflare-go v0.73.0 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 diff --git a/go.sum b/go.sum index 8d6911b544..8f6eaefb5a 100644 --- a/go.sum +++ b/go.sum @@ -181,8 +181,6 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= -github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= -github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= diff --git a/main.go b/main.go index a2d57b81aa..badaf379c1 100644 --- a/main.go +++ b/main.go @@ -399,13 +399,10 @@ func main() { BatchChangeSize: cfg.AWSBatchChangeSize, BatchChangeInterval: cfg.AWSBatchChangeInterval, EvaluateTargetHealth: cfg.AWSEvaluateTargetHealth, - AssumeRole: cfg.AWSAssumeRole, - AssumeRoleExternalID: cfg.AWSAssumeRoleExternalID, - APIRetries: cfg.AWSAPIRetries, PreferCNAME: cfg.AWSPreferCNAME, DryRun: cfg.DryRun, ZoneCacheDuration: cfg.AWSZoneCacheDuration, - }) + }, route53.New(awsSession)) if awsErr != nil { log.Fatal(awsErr) } diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index 8c9107c4e3..e9282fce86 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -48,7 +48,7 @@ func (p FakeWebhookProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []* return endpoints } -func (p FakeWebhookProvider) GetDomainFilter() endpoint.DomainFilterInterface { +func (p FakeWebhookProvider) GetDomainFilter() endpoint.DomainFilter { return endpoint.DomainFilter{} } diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index 2a9c10799f..d80e683092 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -284,6 +284,6 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.End } // GetDomainFilter is the default implementation of GetDomainFilter. -func (p WebhookProvider) GetDomainFilter() endpoint.DomainFilterInterface { +func (p WebhookProvider) GetDomainFilter() endpoint.DomainFilter { return endpoint.DomainFilter{} } From 918b9966825296bd8531bb12eda5a474670b6456 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sat, 12 Aug 2023 17:27:37 +0200 Subject: [PATCH 07/19] fixes to accomodate master changes Signed-off-by: Raffaele Di Fazio --- docs/tutorials/webhook-provider.md | 1 - provider/webhook/httpapi.go | 25 ------------------ provider/webhook/httpapi_test.go | 41 ------------------------------ 3 files changed, 67 deletions(-) diff --git a/docs/tutorials/webhook-provider.md b/docs/tutorials/webhook-provider.md index 1aa20a57b1..63e4dd5abd 100644 --- a/docs/tutorials/webhook-provider.md +++ b/docs/tutorials/webhook-provider.md @@ -20,7 +20,6 @@ The following table represents the methods to implement mapped to their HTTP met | --- | --- | --- | | Records | GET | /records | | ApplyChanges | POST | /records | -| PropertyValuesEqual | POST | /propertyvaluesequal | | AdjustEndpoints | POST | /adjustendpoints | The server needs to respond to those requests by reading the `Accept` header and responding with a corresponding `Content-Type` header specifying the supported media type format and version. diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index b89aee9058..7124de8666 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -77,30 +77,6 @@ func (p *ProviderAPIServer) recordsHandler(w http.ResponseWriter, req *http.Requ } } -func (p *ProviderAPIServer) propertyValuesEqualHandler(w http.ResponseWriter, req *http.Request) { - if req.Method != http.MethodPost { - log.Errorf("Unsupported method %s", req.Method) - w.WriteHeader(http.StatusBadRequest) - return - } - - pve := PropertyValuesEqualsRequest{} - if err := json.NewDecoder(req.Body).Decode(&pve); err != nil { - w.WriteHeader(http.StatusBadRequest) - return - } - - w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - b := p.provider.PropertyValuesEqual(pve.Name, pve.Previous, pve.Current) - r := PropertyValuesEqualsResponse{ - Equals: b, - } - if err := json.NewEncoder(w).Encode(&r); err != nil { - w.WriteHeader(http.StatusInternalServerError) - return - } -} - func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { log.Errorf("Unsupported method %s", req.Method) @@ -136,7 +112,6 @@ func StartHTTPApi(provider provider.Provider, startedChan chan struct{}, readTim m := http.NewServeMux() m.HandleFunc("/records", p.recordsHandler) - m.HandleFunc("/propertyvaluesequal", p.propertyValuesEqualHandler) m.HandleFunc("/adjustendpoints", p.adjustEndpointsHandler) s := &http.Server{ diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index e9282fce86..80bb281b36 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -102,47 +102,6 @@ func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { require.Equal(t, http.StatusNoContent, res.StatusCode) } -func TestPropertyValuesEqualHandlerWithInvalidRequests(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/propertyvaluesequals", nil) - w := httptest.NewRecorder() - - providerAPIServer := &ProviderAPIServer{ - provider: &FakeWebhookProvider{}, - } - providerAPIServer.propertyValuesEqualHandler(w, req) - res := w.Result() - require.Equal(t, http.StatusBadRequest, res.StatusCode) - - req = httptest.NewRequest(http.MethodGet, "/propertyvaluesequals", nil) - - providerAPIServer.propertyValuesEqualHandler(w, req) - res = w.Result() - require.Equal(t, http.StatusBadRequest, res.StatusCode) -} - -func TestPropertyValuesEqualWithValidRequest(t *testing.T) { - pve := &PropertyValuesEqualsRequest{ - Name: "foo", - Previous: "bar", - Current: "baz", - } - - j, err := json.Marshal(pve) - require.Nil(t, err) - - reader := bytes.NewReader(j) - req := httptest.NewRequest(http.MethodPost, "/propertyvaluesequals", reader) - w := httptest.NewRecorder() - - providerAPIServer := &ProviderAPIServer{ - provider: &FakeWebhookProvider{}, - } - providerAPIServer.propertyValuesEqualHandler(w, req) - res := w.Result() - require.Equal(t, http.StatusOK, res.StatusCode) - require.NotNil(t, res.Body) -} - func TestAdjustEndpointsHandlerWithInvalidRequest(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/adjustendpoints", nil) w := httptest.NewRecorder() From e2633398cbcd8232dfcaa64bf8cdfdf1d128145a Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Thu, 24 Aug 2023 15:57:58 +0200 Subject: [PATCH 08/19] remove all propertyvaluesequals leftovers Signed-off-by: Raffaele Di Fazio --- docs/tutorials/webhook-provider.md | 6 +-- provider/webhook/httpapi.go | 28 +++++------- provider/webhook/httpapi_test.go | 14 +++--- provider/webhook/webhook.go | 71 ------------------------------ provider/webhook/webhook_test.go | 21 --------- 5 files changed, 19 insertions(+), 121 deletions(-) diff --git a/docs/tutorials/webhook-provider.md b/docs/tutorials/webhook-provider.md index 63e4dd5abd..900e5b923f 100644 --- a/docs/tutorials/webhook-provider.md +++ b/docs/tutorials/webhook-provider.md @@ -19,12 +19,12 @@ The following table represents the methods to implement mapped to their HTTP met | Provider method | HTTP Method | Route | | --- | --- | --- | | Records | GET | /records | -| ApplyChanges | POST | /records | | AdjustEndpoints | POST | /adjustendpoints | +| ApplyChanges | POST | /records | The server needs to respond to those requests by reading the `Accept` header and responding with a corresponding `Content-Type` header specifying the supported media type format and version. -**NOTE**: only `5xx` responses will be retried and only `200` will be considered as successful. All status codes different from those will be considered a failure on ExternalDNS' side. +**NOTE**: only `5xx` responses will be retried and only `20x` will be considered as successful. All status codes different from those will be considered a failure on ExternalDNS's side. ## Provider registry @@ -40,4 +40,4 @@ To test the Webhook provider and provide a reference implementation, we added th - --run-aws-provider-as-webhook ``` -What will happen behind the scenes is that the AWS provider will be be started as an HTTP server exposed only on localhost and the webhook provider will be configured to talk to it. This is the same setup that we recommend for other providers and a good way to test the Webhook provider other than to serve as a reference implementation. +What will happen behind the scenes is that the AWS provider will be be started as an HTTP server exposed only on localhost and the webhook provider will be configured to talk to it. This is the same setup that we recommend for other providers and a good way to test the Webhook provider. diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index 7124de8666..daddaf2a3d 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -30,21 +30,11 @@ import ( log "github.com/sirupsen/logrus" ) -type ProviderAPIServer struct { +type WebhookServer struct { provider provider.Provider } -type PropertyValuesEqualsRequest struct { - Name string `json:"name"` - Previous string `json:"previous"` - Current string `json:"current"` -} - -type PropertyValuesEqualsResponse struct { - Equals bool `json:"equals"` -} - -func (p *ProviderAPIServer) recordsHandler(w http.ResponseWriter, req *http.Request) { +func (p *WebhookServer) recordsHandler(w http.ResponseWriter, req *http.Request) { switch req.Method { case http.MethodGet: records, err := p.provider.Records(context.Background()) @@ -55,11 +45,14 @@ func (p *ProviderAPIServer) recordsHandler(w http.ResponseWriter, req *http.Requ } w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) w.WriteHeader(http.StatusOK) - json.NewEncoder(w).Encode(records) + if err := json.NewEncoder(w).Encode(records); err != nil { + log.Errorf("Failed to encode records: %v", err) + } return case http.MethodPost: var changes plan.Changes if err := json.NewDecoder(req.Body).Decode(&changes); err != nil { + log.Errorf("Failed to decode changes: %v", err) w.WriteHeader(http.StatusBadRequest) return } @@ -77,7 +70,7 @@ func (p *ProviderAPIServer) recordsHandler(w http.ResponseWriter, req *http.Requ } } -func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *http.Request) { +func (p *WebhookServer) adjustEndpointsHandler(w http.ResponseWriter, req *http.Request) { if req.Method != http.MethodPost { log.Errorf("Unsupported method %s", req.Method) w.WriteHeader(http.StatusBadRequest) @@ -86,12 +79,14 @@ func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *h pve := []*endpoint.Endpoint{} if err := json.NewDecoder(req.Body).Decode(&pve); err != nil { + log.Errorf("Failed to decode in adjustEndpointsHandler: %v", err) w.WriteHeader(http.StatusBadRequest) return } w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) pve = p.provider.AdjustEndpoints(pve) if err := json.NewEncoder(w).Encode(&pve); err != nil { + log.Errorf("Failed to encode in adjustEndpointsHandler: %v", err) w.WriteHeader(http.StatusInternalServerError) return } @@ -99,14 +94,13 @@ func (p *ProviderAPIServer) adjustEndpointsHandler(w http.ResponseWriter, req *h // StartHTTPApi starts a HTTP server given any provider. // the function takes an optional channel as input which is used to signal that the server has started. -// The server will listen on port 8888. +// The server will listen on port `providerPort`. // The server will respond to the following endpoints: // - /records (GET): returns the current records // - /records (POST): applies the changes -// - /propertyvaluesequal (POST): executes the PropertyValuesEqual method // - /adjustendpoints (POST): executes the AdjustEndpoints method func StartHTTPApi(provider provider.Provider, startedChan chan struct{}, readTimeout, writeTimeout time.Duration, providerPort string) { - p := ProviderAPIServer{ + p := WebhookServer{ provider: provider, } diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index 80bb281b36..1ae5336f92 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -40,10 +40,6 @@ func (p FakeWebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Cha return nil } -func (p FakeWebhookProvider) PropertyValuesEqual(name string, previous string, current string) bool { - return false -} - func (p FakeWebhookProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { return endpoints } @@ -56,7 +52,7 @@ func TestRecordsHandlerRecords(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/records", nil) w := httptest.NewRecorder() - providerAPIServer := &ProviderAPIServer{ + providerAPIServer := &WebhookServer{ provider: &FakeWebhookProvider{}, } providerAPIServer.recordsHandler(w, req) @@ -68,7 +64,7 @@ func TestRecordsHandlerApplyChangesWithBadRequest(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/applychanges", nil) w := httptest.NewRecorder() - providerAPIServer := &ProviderAPIServer{ + providerAPIServer := &WebhookServer{ provider: &FakeWebhookProvider{}, } providerAPIServer.recordsHandler(w, req) @@ -94,7 +90,7 @@ func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/applychanges", reader) w := httptest.NewRecorder() - providerAPIServer := &ProviderAPIServer{ + providerAPIServer := &WebhookServer{ provider: &FakeWebhookProvider{}, } providerAPIServer.recordsHandler(w, req) @@ -106,7 +102,7 @@ func TestAdjustEndpointsHandlerWithInvalidRequest(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/adjustendpoints", nil) w := httptest.NewRecorder() - providerAPIServer := &ProviderAPIServer{ + providerAPIServer := &WebhookServer{ provider: &FakeWebhookProvider{}, } providerAPIServer.adjustEndpointsHandler(w, req) @@ -137,7 +133,7 @@ func TestAdjustEndpointsWithValidRequest(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/adjustendpoints", reader) w := httptest.NewRecorder() - providerAPIServer := &ProviderAPIServer{ + providerAPIServer := &WebhookServer{ provider: &FakeWebhookProvider{}, } providerAPIServer.adjustEndpointsHandler(w, req) diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index d80e683092..0a50985c9d 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -56,14 +56,6 @@ var ( Help: "Errors with ApplyChanges method", }, ) - propertyValuesEqualErrorsGauge = prometheus.NewGauge( - prometheus.GaugeOpts{ - Namespace: "external_dns", - Subsystem: "webhook_provider", - Name: "propertyvaluesequal_errors", - Help: "Errors with PropertyValuesEqual method", - }, - ) adjustEndpointsErrorsGauge = prometheus.NewGauge( prometheus.GaugeOpts{ Namespace: "external_dns", @@ -79,20 +71,9 @@ type WebhookProvider struct { remoteServerURL *url.URL } -type PropertyValuesEqualRequest struct { - Name string `json:"name"` - Previous string `json:"previous"` - Current string `json:"current"` -} - -type PropertyValuesEqualResponse struct { - Equals bool `json:"equals"` -} - func init() { prometheus.MustRegister(recordsErrorsGauge) prometheus.MustRegister(applyChangesErrorsGauge) - prometheus.MustRegister(propertyValuesEqualErrorsGauge) prometheus.MustRegister(adjustEndpointsErrorsGauge) } @@ -179,58 +160,6 @@ func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes return nil } -// PropertyValuesEqual will call the provider doing a POST on `/propertyvaluesequal` which will return a boolean in the format -// `{propertyvaluesequal: true}` -// Errors in anything technically happening from the provider will return true so that no update is performed. -// Errors will also be logged and exposed as metrics so that it is possible to alert on them if needed. -func (p WebhookProvider) PropertyValuesEqual(name string, previous string, current string) bool { - u := p.remoteServerURL.JoinPath("records").String() - - b := new(bytes.Buffer) - if err := json.NewEncoder(b).Encode(&PropertyValuesEqualRequest{ - Name: name, - Previous: previous, - Current: current, - }); err != nil { - adjustEndpointsErrorsGauge.Inc() - log.Debugf("Failed to encode, %s", err) - return true - } - - req, err := http.NewRequest("POST", u, b) - if err != nil { - propertyValuesEqualErrorsGauge.Inc() - log.Debugf("Failed to create request: %s", err) - return true - } - - req.Header.Set(contentTypeHeader, mediaTypeFormatAndVersion) - req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) - - resp, err := p.client.Do(req) - if err != nil { - propertyValuesEqualErrorsGauge.Inc() - log.Debugf("Failed to perform request: %s", err) - return true - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - propertyValuesEqualErrorsGauge.Inc() - log.Debugf("Failed to run PropertyValuesEqual with code %d", resp.StatusCode) - return true - } - - r := PropertyValuesEqualResponse{} - if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { - recordsErrorsGauge.Inc() - log.Debugf("Failed to decode response body: %s", err.Error()) - return true - } - - return r.Equals -} - // AdjustEndpoints will call the provider doing a POST on `/adjustendpoints` which will return a list of modified endpoints // based on a provider specific requirement. // This method returns an empty slice in case there is a technical error on the provider's side so that no endpoints will be considered. diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index c903307dfb..0e8d3add46 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -80,27 +80,6 @@ func TestApplyChanges(t *testing.T) { require.NotNil(t, err) } -func TestPropertyValuesEqual(t *testing.T) { - svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - w.Header().Set(varyHeader, contentTypeHeader) - w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - w.WriteHeader(200) - return - } - j, _ := json.Marshal(&PropertyValuesEqualResponse{ - Equals: false, - }) - w.Write(j) - })) - defer svr.Close() - - provider, err := NewWebhookProvider(svr.URL) - require.Nil(t, err) - b := provider.PropertyValuesEqual("name", "previous", "current") - require.Equal(t, false, b) -} - func TestAdjustEndpoints(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { From 8f02bebb5ec77316932c621816d9b5eef6a9c23a Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Fri, 25 Aug 2023 17:09:26 +0200 Subject: [PATCH 09/19] readd negotiation to pass the domain filter around Signed-off-by: Raffaele Di Fazio --- go.mod | 1 + go.sum | 10 ++++++ provider/webhook/httpapi.go | 15 ++++++++- provider/webhook/httpapi_test.go | 10 +++++- provider/webhook/webhook.go | 55 ++++++++++++++++++++++++++++++-- provider/webhook/webhook_test.go | 39 ++++++++++++++++++++-- 6 files changed, 123 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 9c97eac090..278abd70a6 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/ans-group/sdk-go v1.16.6 github.com/aws/aws-sdk-go v1.44.311 github.com/bodgit/tsig v1.2.2 + github.com/cenkalti/backoff/v4 v4.2.1 github.com/civo/civogo v0.3.42 github.com/cloudflare/cloudflare-go v0.73.0 github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 diff --git a/go.sum b/go.sum index 8f6eaefb5a..3dd743f38e 100644 --- a/go.sum +++ b/go.sum @@ -180,7 +180,10 @@ github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8n github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -351,6 +354,7 @@ github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgO github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -510,6 +514,7 @@ github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkY github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0= github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E= @@ -568,6 +573,7 @@ github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qK github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.1.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= github.com/googleapis/gnostic v0.3.1/go.mod h1:on+2t9HRStVgn95RSsFWFz+6Q0Snyqv1awfrALZdbtU= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gookit/color v1.2.3/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= @@ -684,6 +690,7 @@ github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqx github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -698,6 +705,7 @@ github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVY github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= @@ -754,6 +762,7 @@ github.com/linode/linodego v1.19.0/go.mod h1:XZFR+yJ9mm2kwf6itZ6SCpu+6w3KnIevV0U github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lyft/protoc-gen-star v0.4.10/go.mod h1:mE8fbna26u7aEA2QCVvvfBU/ZrPgocG1206xAFPcs94= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= @@ -830,6 +839,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index daddaf2a3d..74b2f4cdae 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -31,7 +31,8 @@ import ( ) type WebhookServer struct { - provider provider.Provider + provider provider.Provider + domainFilter endpoint.DomainFilter } func (p *WebhookServer) recordsHandler(w http.ResponseWriter, req *http.Request) { @@ -92,6 +93,17 @@ func (p *WebhookServer) adjustEndpointsHandler(w http.ResponseWriter, req *http. } } +func (p *WebhookServer) negotiateHandler(w http.ResponseWriter, req *http.Request) { + b, err := p.domainFilter.MarshalJSON() + if err != nil { + log.Errorf("Failed to marshal domain filter: %v", err) + w.WriteHeader(http.StatusInternalServerError) + return + } + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.Write(b) +} + // StartHTTPApi starts a HTTP server given any provider. // the function takes an optional channel as input which is used to signal that the server has started. // The server will listen on port `providerPort`. @@ -105,6 +117,7 @@ func StartHTTPApi(provider provider.Provider, startedChan chan struct{}, readTim } m := http.NewServeMux() + m.HandleFunc("/", p.negotiateHandler) m.HandleFunc("/records", p.recordsHandler) m.HandleFunc("/adjustendpoints", p.adjustEndpointsHandler) diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index 1ae5336f92..f260a8e0e9 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "io" "net/http" "net/http/httptest" "testing" @@ -146,6 +147,13 @@ func TestStartHTTPApi(t *testing.T) { startedChan := make(chan struct{}) go StartHTTPApi(FakeWebhookProvider{}, startedChan, 5*time.Second, 10*time.Second, "127.0.0.1:8887") <-startedChan - _, err := http.Get("http://127.0.0.1:8887") + resp, err := http.Get("http://127.0.0.1:8887") require.NoError(t, err) + // check that resp has a valid domain filter + defer resp.Body.Close() + + df := endpoint.DomainFilter{} + b, err := io.ReadAll(resp.Body) + require.NoError(t, err) + require.NoError(t, df.UnmarshalJSON(b)) } diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index 0a50985c9d..db6f8c6829 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -21,12 +21,14 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "net/url" "sigs.k8s.io/external-dns/endpoint" "sigs.k8s.io/external-dns/plan" + backoff "github.com/cenkalti/backoff/v4" "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" ) @@ -69,6 +71,7 @@ var ( type WebhookProvider struct { client *http.Client remoteServerURL *url.URL + DomainFilter endpoint.DomainFilter } func init() { @@ -83,11 +86,59 @@ func NewWebhookProvider(u string) (*WebhookProvider, error) { return nil, err } + // negotiate API information + req, err := http.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + req.Header.Set(acceptHeader, mediaTypeFormatAndVersion) + client := &http.Client{} + var resp *http.Response + err = backoff.Retry(func() error { + resp, err = client.Do(req) + if err != nil { + log.Debugf("Failed to connect to plugin api: %v", err) + return err + } + // we currently only use 200 as success, but considering okay all 2XX for future usage + if resp.StatusCode >= 300 && resp.StatusCode < 500 { + return backoff.Permanent(fmt.Errorf("status code < 500")) + } + return nil + }, backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries)) + + if err != nil { + return nil, fmt.Errorf("failed to connect to plugin api: %v", err) + } + + vary := resp.Header.Get(varyHeader) + contentType := resp.Header.Get(contentTypeHeader) + + // read the serialized DomainFilter from the response body and set it in the webhook provider struct + defer resp.Body.Close() + + df := endpoint.DomainFilter{} + b, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response body: %v", err) + } + if err := df.UnmarshalJSON(b); err != nil { + return nil, fmt.Errorf("failed to unmarshal response body of DomainFilter: %v", err) + } + + if vary != contentTypeHeader { + return nil, fmt.Errorf("wrong vary value returned from server: %s", vary) + } + + if contentType != mediaTypeFormatAndVersion { + return nil, fmt.Errorf("wrong content type returned from server: %s", contentType) + } return &WebhookProvider{ client: client, remoteServerURL: parsedURL, + DomainFilter: df, }, nil } @@ -212,7 +263,7 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.End return endpoints } -// GetDomainFilter is the default implementation of GetDomainFilter. +// GetDomainFilter make calls to get the serialized version of the domain filter func (p WebhookProvider) GetDomainFilter() endpoint.DomainFilter { - return endpoint.DomainFilter{} + return p.DomainFilter } diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index 0e8d3add46..785bb9e28b 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/external-dns/endpoint" ) -func TestRecords(t *testing.T) { +func TestInvalidDomainFilter(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { w.Header().Set(varyHeader, contentTypeHeader) @@ -42,6 +42,39 @@ func TestRecords(t *testing.T) { })) defer svr.Close() + _, err := NewWebhookProvider(svr.URL) + require.Error(t, err) +} + +func TestValidDomainfilter(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(varyHeader, contentTypeHeader) + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.Write([]byte(`{}`)) + return + } + })) + defer svr.Close() + + _, err := NewWebhookProvider(svr.URL) + require.NoError(t, err) +} + +func TestRecords(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(varyHeader, contentTypeHeader) + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.Write([]byte(`{}`)) + return + } + w.Write([]byte(`[{ + "dnsName" : "test.example.com" + }]`)) + })) + defer svr.Close() + provider, err := NewWebhookProvider(svr.URL) require.Nil(t, err) endpoints, err := provider.Records(context.TODO()) @@ -58,7 +91,7 @@ func TestApplyChanges(t *testing.T) { if r.URL.Path == "/" { w.Header().Set(varyHeader, contentTypeHeader) w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - w.WriteHeader(200) + w.Write([]byte(`{}`)) return } if successfulApplyChanges { @@ -85,7 +118,7 @@ func TestAdjustEndpoints(t *testing.T) { if r.URL.Path == "/" { w.Header().Set(varyHeader, contentTypeHeader) w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - w.WriteHeader(200) + w.Write([]byte(`{}`)) return } var endpoints []*endpoint.Endpoint From 36b29bf6a5016972c7b076655a1fddc9984e3d9b Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sat, 26 Aug 2023 16:02:55 +0200 Subject: [PATCH 10/19] fix domain filter passing Signed-off-by: Raffaele Di Fazio --- provider/webhook/httpapi.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index 74b2f4cdae..62d03edf16 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -31,8 +31,7 @@ import ( ) type WebhookServer struct { - provider provider.Provider - domainFilter endpoint.DomainFilter + provider provider.Provider } func (p *WebhookServer) recordsHandler(w http.ResponseWriter, req *http.Request) { @@ -94,7 +93,7 @@ func (p *WebhookServer) adjustEndpointsHandler(w http.ResponseWriter, req *http. } func (p *WebhookServer) negotiateHandler(w http.ResponseWriter, req *http.Request) { - b, err := p.domainFilter.MarshalJSON() + b, err := p.provider.GetDomainFilter().MarshalJSON() if err != nil { log.Errorf("Failed to marshal domain filter: %v", err) w.WriteHeader(http.StatusInternalServerError) From 847682227407bc768ba9d20fd466acd60f1ba830 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sun, 27 Aug 2023 16:14:28 +0200 Subject: [PATCH 11/19] webhook fixes Signed-off-by: Raffaele Di Fazio --- main.go | 2 +- provider/webhook/httpapi.go | 1 + provider/webhook/webhook.go | 6 ------ 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/main.go b/main.go index 22fc79221f..2b867add8d 100644 --- a/main.go +++ b/main.go @@ -186,7 +186,7 @@ func main() { zoneTagFilter := provider.NewZoneTagFilter(cfg.AWSZoneTagFilter) var awsSession *session.Session - if cfg.Provider == "aws" || cfg.Provider == "aws-sd" || cfg.Registry == "dynamodb" { + if cfg.Provider == "aws" || cfg.Provider == "aws-sd" || cfg.Registry == "dynamodb" || cfg.RunAWSProviderAsWebhook { awsSession, err = aws.NewSession( aws.AWSSessionConfig{ AssumeRole: cfg.AWSAssumeRole, diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index 62d03edf16..1c19c524fb 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -107,6 +107,7 @@ func (p *WebhookServer) negotiateHandler(w http.ResponseWriter, req *http.Reques // the function takes an optional channel as input which is used to signal that the server has started. // The server will listen on port `providerPort`. // The server will respond to the following endpoints: +// - / (GET): initialization, negotiates headers and returns the domain filter // - /records (GET): returns the current records // - /records (POST): applies the changes // - /adjustendpoints (POST): executes the AdjustEndpoints method diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index db6f8c6829..a6fc112964 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -37,7 +37,6 @@ const ( mediaTypeFormatAndVersion = "application/external.dns.webhook+json;version=1" contentTypeHeader = "Content-Type" acceptHeader = "Accept" - varyHeader = "Vary" maxRetries = 5 ) @@ -112,7 +111,6 @@ func NewWebhookProvider(u string) (*WebhookProvider, error) { return nil, fmt.Errorf("failed to connect to plugin api: %v", err) } - vary := resp.Header.Get(varyHeader) contentType := resp.Header.Get(contentTypeHeader) // read the serialized DomainFilter from the response body and set it in the webhook provider struct @@ -127,10 +125,6 @@ func NewWebhookProvider(u string) (*WebhookProvider, error) { return nil, fmt.Errorf("failed to unmarshal response body of DomainFilter: %v", err) } - if vary != contentTypeHeader { - return nil, fmt.Errorf("wrong vary value returned from server: %s", vary) - } - if contentType != mediaTypeFormatAndVersion { return nil, fmt.Errorf("wrong content type returned from server: %s", contentType) } From 38d671aa355e54fd7444fd55524d332800132d40 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Thu, 31 Aug 2023 17:48:08 +0200 Subject: [PATCH 12/19] fix tests Signed-off-by: Raffaele Di Fazio --- provider/webhook/webhook_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index 785bb9e28b..b8c0550eb2 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -31,7 +31,6 @@ import ( func TestInvalidDomainFilter(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - w.Header().Set(varyHeader, contentTypeHeader) w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) w.WriteHeader(200) return @@ -49,7 +48,6 @@ func TestInvalidDomainFilter(t *testing.T) { func TestValidDomainfilter(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - w.Header().Set(varyHeader, contentTypeHeader) w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) w.Write([]byte(`{}`)) return @@ -64,7 +62,6 @@ func TestValidDomainfilter(t *testing.T) { func TestRecords(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - w.Header().Set(varyHeader, contentTypeHeader) w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) w.Write([]byte(`{}`)) return @@ -89,7 +86,6 @@ func TestApplyChanges(t *testing.T) { successfulApplyChanges := true svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - w.Header().Set(varyHeader, contentTypeHeader) w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) w.Write([]byte(`{}`)) return @@ -116,7 +112,6 @@ func TestApplyChanges(t *testing.T) { func TestAdjustEndpoints(t *testing.T) { svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { - w.Header().Set(varyHeader, contentTypeHeader) w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) w.Write([]byte(`{}`)) return From d58bb11d6b201359adaef39da7d594c63c8ab6ac Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Thu, 31 Aug 2023 18:06:40 +0200 Subject: [PATCH 13/19] fix docs Signed-off-by: Raffaele Di Fazio --- docs/tutorials/webhook-provider.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tutorials/webhook-provider.md b/docs/tutorials/webhook-provider.md index 900e5b923f..44adab6f5a 100644 --- a/docs/tutorials/webhook-provider.md +++ b/docs/tutorials/webhook-provider.md @@ -22,6 +22,8 @@ The following table represents the methods to implement mapped to their HTTP met | AdjustEndpoints | POST | /adjustendpoints | | ApplyChanges | POST | /records | +ExternalDNS will also make requests to the `/` endpoint for negotatiation and for deseliarization of the `DomainFilter`. + The server needs to respond to those requests by reading the `Accept` header and responding with a corresponding `Content-Type` header specifying the supported media type format and version. **NOTE**: only `5xx` responses will be retried and only `20x` will be considered as successful. All status codes different from those will be considered a failure on ExternalDNS's side. @@ -30,7 +32,6 @@ The server needs to respond to those requests by reading the `Accept` header and To simplify the discovery of providers, we will accept pull requests that will add links to providers in the [README](../../README.md) file. This list will serve the only purpose of simplifying finding providers and will not constitute an official endorsment of any of the externally implemented providers unless otherwise specified. - ## Run the AWS provider with the webhook provider. To test the Webhook provider and provide a reference implementation, we added the functionality to run the AWS provider as a webhook. To run the AWS provider as a webhook, you need the following flags: From 590dafd93e684354c98295a2764eb101babd2efb Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Fri, 15 Sep 2023 18:43:01 +0200 Subject: [PATCH 14/19] docs fixes Signed-off-by: Raffaele Di Fazio --- docs/tutorials/webhook-provider.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/tutorials/webhook-provider.md b/docs/tutorials/webhook-provider.md index 44adab6f5a..5f5b4ceb70 100644 --- a/docs/tutorials/webhook-provider.md +++ b/docs/tutorials/webhook-provider.md @@ -1,8 +1,8 @@ # Webhook provider -The "Webhook" provider allows to integrate ExternalDNS with DNS providers via an HTTP interface. -The Webhook provider implements the Provider interface. Instead of implementing code specific to a provider, it implements an HTTP client that sends request to an HTTP API. -The idea behind it is that providers can be implemented in a separate program: these programs expose an HTTP API that the Webhook provider can interact with. The ideal setup for providers is to run as sidecars in the same pod of the ExternalDNS container and listen on localhost only. This is not strictly a requirement, but we would not recommend other setups. +The "Webhook" provider allows integrating ExternalDNS with DNS providers through an HTTP interface. +The Webhook provider implements the `Provider` interface. Instead of implementing code specific to a provider, it implements an HTTP client that sends requests to an HTTP API. +The idea behind it is that providers can be implemented in separate programs: these programs expose an HTTP API that the Webhook provider interacts with. The ideal setup for providers is to run as a sidecar in the same pod of the ExternalDNS container, listening only on localhost. This is not strictly a requirement, but we do not recommend other setups. ## Architectural diagram @@ -10,7 +10,7 @@ The idea behind it is that providers can be implemented in a separate program: t ## API guarantees -Providers implementing the HTTP API have to keep in sync with changes to the Go types `plan.Changes` and `endpoint.Endpoint`. We do not expect to make significant changes to those types given the maturity of the project, but we can't exclude that changes will need to happen. We commit to publishing changes to those in the release notes, to ensure that providers implementing the API can keep providers up to date quickly. +Providers implementing the HTTP API have to keep in sync with changes to the JSON serialization of Go types `plan.Changes`, `endpoint.Endpoint`, and `endpoint.DomainFilter`. Given the maturity of the project, we do not expect to make significant changes to those types, but can't exclude the possibility that changes will need to happen. We commit to publishing changes to those in the release notes, to ensure that providers implementing the API can keep providers up to date quickly. ## Implementation requirements @@ -30,7 +30,7 @@ The server needs to respond to those requests by reading the `Accept` header and ## Provider registry -To simplify the discovery of providers, we will accept pull requests that will add links to providers in the [README](../../README.md) file. This list will serve the only purpose of simplifying finding providers and will not constitute an official endorsment of any of the externally implemented providers unless otherwise specified. +To simplify the discovery of providers, we will accept pull requests that will add links to providers in the [README](../../README.md) file. This list will only serve the purpose of simplifying finding providers and will not constitute an official endorsement of any of the externally implemented providers unless otherwise stated. ## Run the AWS provider with the webhook provider. From ee56947ea84ff2692d96e96ac7a8d068e8955995 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Fri, 15 Sep 2023 18:59:13 +0200 Subject: [PATCH 15/19] code review comments on json unmarshal Signed-off-by: Raffaele Di Fazio --- provider/webhook/httpapi.go | 8 +------- provider/webhook/webhook.go | 7 +------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index 1c19c524fb..fc21d47bb8 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -93,14 +93,8 @@ func (p *WebhookServer) adjustEndpointsHandler(w http.ResponseWriter, req *http. } func (p *WebhookServer) negotiateHandler(w http.ResponseWriter, req *http.Request) { - b, err := p.provider.GetDomainFilter().MarshalJSON() - if err != nil { - log.Errorf("Failed to marshal domain filter: %v", err) - w.WriteHeader(http.StatusInternalServerError) - return - } w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - w.Write(b) + json.NewEncoder(w).Encode(p.provider.GetDomainFilter()) } // StartHTTPApi starts a HTTP server given any provider. diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index a6fc112964..69b2886f80 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -21,7 +21,6 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" "net/url" @@ -117,11 +116,7 @@ func NewWebhookProvider(u string) (*WebhookProvider, error) { defer resp.Body.Close() df := endpoint.DomainFilter{} - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) - } - if err := df.UnmarshalJSON(b); err != nil { + if err := json.NewDecoder(resp.Body).Decode(&df); err != nil { return nil, fmt.Errorf("failed to unmarshal response body of DomainFilter: %v", err) } From 7e35d6c09c02e67d144662b0861493db9e8fe642 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Fri, 15 Sep 2023 21:10:07 +0200 Subject: [PATCH 16/19] handle error in adjustendpoints Signed-off-by: Raffaele Di Fazio --- provider/webhook/httpapi.go | 6 +++++- provider/webhook/httpapi_test.go | 4 ++-- provider/webhook/webhook.go | 16 ++++++++-------- provider/webhook/webhook_test.go | 3 ++- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/provider/webhook/httpapi.go b/provider/webhook/httpapi.go index fc21d47bb8..8075ea2ca9 100644 --- a/provider/webhook/httpapi.go +++ b/provider/webhook/httpapi.go @@ -84,7 +84,11 @@ func (p *WebhookServer) adjustEndpointsHandler(w http.ResponseWriter, req *http. return } w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - pve = p.provider.AdjustEndpoints(pve) + pve, err := p.provider.AdjustEndpoints(pve) + if err != nil { + log.Errorf("Failed to call adjust endpoints: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } if err := json.NewEncoder(w).Encode(&pve); err != nil { log.Errorf("Failed to encode in adjustEndpointsHandler: %v", err) w.WriteHeader(http.StatusInternalServerError) diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index f260a8e0e9..d27185e690 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -41,8 +41,8 @@ func (p FakeWebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Cha return nil } -func (p FakeWebhookProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) []*endpoint.Endpoint { - return endpoints +func (p FakeWebhookProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { + return endpoints, nil } func (p FakeWebhookProvider) GetDomainFilter() endpoint.DomainFilter { diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index 69b2886f80..5b875930a9 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -203,27 +203,27 @@ func (p WebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes // AdjustEndpoints will call the provider doing a POST on `/adjustendpoints` which will return a list of modified endpoints // based on a provider specific requirement. // This method returns an empty slice in case there is a technical error on the provider's side so that no endpoints will be considered. -func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.Endpoint { +func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { endpoints := []*endpoint.Endpoint{} u, err := url.JoinPath(p.remoteServerURL.String(), "adjustendpoints") if err != nil { adjustEndpointsErrorsGauge.Inc() log.Debugf("Failed to join path, %s", err) - return endpoints + return nil, err } b := new(bytes.Buffer) if err := json.NewEncoder(b).Encode(e); err != nil { adjustEndpointsErrorsGauge.Inc() log.Debugf("Failed to encode endpoints, %s", err) - return endpoints + return nil, err } req, err := http.NewRequest("POST", u, b) if err != nil { adjustEndpointsErrorsGauge.Inc() log.Debugf("Failed to create new HTTP request, %s", err) - return endpoints + return nil, err } req.Header.Set(contentTypeHeader, mediaTypeFormatAndVersion) @@ -233,23 +233,23 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) []*endpoint.End if err != nil { adjustEndpointsErrorsGauge.Inc() log.Debugf("Failed executing http request, %s", err) - return endpoints + return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { adjustEndpointsErrorsGauge.Inc() log.Debugf("Failed to AdjustEndpoints with code %d", resp.StatusCode) - return endpoints + return nil, err } if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { recordsErrorsGauge.Inc() log.Debugf("Failed to decode response body: %s", err.Error()) - return endpoints + return nil, err } - return endpoints + return endpoints, nil } // GetDomainFilter make calls to get the serialized version of the domain filter diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index b8c0550eb2..a243fda832 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -148,7 +148,8 @@ func TestAdjustEndpoints(t *testing.T) { }, }, } - adjustedEndpoints := provider.AdjustEndpoints(endpoints) + adjustedEndpoints, err := provider.AdjustEndpoints(endpoints) + require.NoError(t, err) require.Equal(t, []*endpoint.Endpoint{{ DNSName: "test.example.com", RecordTTL: 0, From 62540d2cfa03754c2947946b2e6da40d529dd0c5 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sat, 16 Sep 2023 10:01:08 +0200 Subject: [PATCH 17/19] fix a bunch of wrong require Signed-off-by: Raffaele Di Fazio --- provider/webhook/httpapi_test.go | 4 ++-- provider/webhook/webhook_test.go | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index d27185e690..878bcdc796 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -84,7 +84,7 @@ func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { }, } j, err := json.Marshal(changes) - require.Nil(t, err) + require.NoError(t, err) reader := bytes.NewReader(j) @@ -128,7 +128,7 @@ func TestAdjustEndpointsWithValidRequest(t *testing.T) { } j, err := json.Marshal(pve) - require.Nil(t, err) + require.NoError(t, err) reader := bytes.NewReader(j) req := httptest.NewRequest(http.MethodPost, "/adjustendpoints", reader) diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index a243fda832..011d66d65c 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -73,9 +73,9 @@ func TestRecords(t *testing.T) { defer svr.Close() provider, err := NewWebhookProvider(svr.URL) - require.Nil(t, err) + require.NoError(t, err) endpoints, err := provider.Records(context.TODO()) - require.Nil(t, err) + require.NoError(t, err) require.NotNil(t, endpoints) require.Equal(t, []*endpoint.Endpoint{{ DNSName: "test.example.com", @@ -99,9 +99,9 @@ func TestApplyChanges(t *testing.T) { defer svr.Close() provider, err := NewWebhookProvider(svr.URL) - require.Nil(t, err) + require.NoError(t, err) err = provider.ApplyChanges(context.TODO(), nil) - require.Nil(t, err) + require.NoError(t, err) successfulApplyChanges = false @@ -137,7 +137,7 @@ func TestAdjustEndpoints(t *testing.T) { defer svr.Close() provider, err := NewWebhookProvider(svr.URL) - require.Nil(t, err) + require.NoError(t, err) endpoints := []*endpoint.Endpoint{ { DNSName: "test.example.com", From e4bd3ad241a1d3ea1a30781f40cd449710615556 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Sat, 16 Sep 2023 19:30:30 +0200 Subject: [PATCH 18/19] tests and docs Signed-off-by: Raffaele Di Fazio --- docs/tutorials/webhook-provider.md | 2 +- provider/webhook/httpapi_test.go | 129 +++++++++++++++++++++++++++-- provider/webhook/webhook.go | 2 +- provider/webhook/webhook_test.go | 56 ++++++++++++- 4 files changed, 180 insertions(+), 9 deletions(-) diff --git a/docs/tutorials/webhook-provider.md b/docs/tutorials/webhook-provider.md index 5f5b4ceb70..9cd1164703 100644 --- a/docs/tutorials/webhook-provider.md +++ b/docs/tutorials/webhook-provider.md @@ -22,7 +22,7 @@ The following table represents the methods to implement mapped to their HTTP met | AdjustEndpoints | POST | /adjustendpoints | | ApplyChanges | POST | /records | -ExternalDNS will also make requests to the `/` endpoint for negotatiation and for deseliarization of the `DomainFilter`. +ExternalDNS will also make requests to the `/` endpoint for negotiation and for deserialization of the `DomainFilter`. The server needs to respond to those requests by reading the `Accept` header and responding with a corresponding `Content-Type` header specifying the supported media type format and version. diff --git a/provider/webhook/httpapi_test.go b/provider/webhook/httpapi_test.go index 878bcdc796..70ee28cb06 100644 --- a/provider/webhook/httpapi_test.go +++ b/provider/webhook/httpapi_test.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net/http" "net/http/httptest" @@ -31,22 +32,48 @@ import ( "sigs.k8s.io/external-dns/plan" ) -type FakeWebhookProvider struct{} +var records []*endpoint.Endpoint + +type FakeWebhookProvider struct { + err error + domainFilter endpoint.DomainFilter +} func (p FakeWebhookProvider) Records(ctx context.Context) ([]*endpoint.Endpoint, error) { - return []*endpoint.Endpoint{}, nil + if p.err != nil { + return nil, p.err + } + return records, nil } func (p FakeWebhookProvider) ApplyChanges(ctx context.Context, changes *plan.Changes) error { + if p.err != nil { + return p.err + } + records = append(records, changes.Create...) return nil } func (p FakeWebhookProvider) AdjustEndpoints(endpoints []*endpoint.Endpoint) ([]*endpoint.Endpoint, error) { + // for simplicity, we do not adjust endpoints in this test + if p.err != nil { + return nil, p.err + } return endpoints, nil } func (p FakeWebhookProvider) GetDomainFilter() endpoint.DomainFilter { - return endpoint.DomainFilter{} + return p.domainFilter +} + +func TestMain(m *testing.M) { + records = []*endpoint.Endpoint{ + { + DNSName: "foo.bar.com", + RecordType: "A", + }, + } + m.Run() } func TestRecordsHandlerRecords(t *testing.T) { @@ -54,11 +81,35 @@ func TestRecordsHandlerRecords(t *testing.T) { w := httptest.NewRecorder() providerAPIServer := &WebhookServer{ - provider: &FakeWebhookProvider{}, + provider: &FakeWebhookProvider{ + domainFilter: endpoint.NewDomainFilter([]string{"foo.bar.com"}), + }, } providerAPIServer.recordsHandler(w, req) res := w.Result() require.Equal(t, http.StatusOK, res.StatusCode) + // require that the res has the same endpoints as the records slice + defer res.Body.Close() + require.NotNil(t, res.Body) + endpoints := []*endpoint.Endpoint{} + if err := json.NewDecoder(res.Body).Decode(&endpoints); err != nil { + t.Errorf("Failed to decode response body: %s", err.Error()) + } + require.Equal(t, records, endpoints) +} + +func TestRecordsHandlerRecordsWithErrors(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/records", nil) + w := httptest.NewRecorder() + + providerAPIServer := &WebhookServer{ + provider: &FakeWebhookProvider{ + err: fmt.Errorf("error"), + }, + } + providerAPIServer.recordsHandler(w, req) + res := w.Result() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) } func TestRecordsHandlerApplyChangesWithBadRequest(t *testing.T) { @@ -99,6 +150,46 @@ func TestRecordsHandlerApplyChangesWithValidRequest(t *testing.T) { require.Equal(t, http.StatusNoContent, res.StatusCode) } +func TestRecordsHandlerApplyChangesWithErrors(t *testing.T) { + changes := &plan.Changes{ + Create: []*endpoint.Endpoint{ + { + DNSName: "foo.bar.com", + RecordType: "A", + Targets: endpoint.Targets{}, + }, + }, + } + j, err := json.Marshal(changes) + require.NoError(t, err) + + reader := bytes.NewReader(j) + + req := httptest.NewRequest(http.MethodPost, "/applychanges", reader) + w := httptest.NewRecorder() + + providerAPIServer := &WebhookServer{ + provider: &FakeWebhookProvider{ + err: fmt.Errorf("error"), + }, + } + providerAPIServer.recordsHandler(w, req) + res := w.Result() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) +} + +func TestRecordsHandlerWithWrongHTTPMethod(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/records", nil) + w := httptest.NewRecorder() + + providerAPIServer := &WebhookServer{ + provider: &FakeWebhookProvider{}, + } + providerAPIServer.recordsHandler(w, req) + res := w.Result() + require.Equal(t, http.StatusBadRequest, res.StatusCode) +} + func TestAdjustEndpointsHandlerWithInvalidRequest(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/adjustendpoints", nil) w := httptest.NewRecorder() @@ -117,7 +208,7 @@ func TestAdjustEndpointsHandlerWithInvalidRequest(t *testing.T) { require.Equal(t, http.StatusBadRequest, res.StatusCode) } -func TestAdjustEndpointsWithValidRequest(t *testing.T) { +func TestAdjustEndpointsHandlerWithValidRequest(t *testing.T) { pve := []*endpoint.Endpoint{ { DNSName: "foo.bar.com", @@ -143,6 +234,34 @@ func TestAdjustEndpointsWithValidRequest(t *testing.T) { require.NotNil(t, res.Body) } +func TestAdjustEndpointsHandlerWithError(t *testing.T) { + pve := []*endpoint.Endpoint{ + { + DNSName: "foo.bar.com", + RecordType: "A", + Targets: endpoint.Targets{}, + RecordTTL: 0, + }, + } + + j, err := json.Marshal(pve) + require.NoError(t, err) + + reader := bytes.NewReader(j) + req := httptest.NewRequest(http.MethodPost, "/adjustendpoints", reader) + w := httptest.NewRecorder() + + providerAPIServer := &WebhookServer{ + provider: &FakeWebhookProvider{ + err: fmt.Errorf("error"), + }, + } + providerAPIServer.adjustEndpointsHandler(w, req) + res := w.Result() + require.Equal(t, http.StatusInternalServerError, res.StatusCode) + require.NotNil(t, res.Body) +} + func TestStartHTTPApi(t *testing.T) { startedChan := make(chan struct{}) go StartHTTPApi(FakeWebhookProvider{}, startedChan, 5*time.Second, 10*time.Second, "127.0.0.1:8887") diff --git a/provider/webhook/webhook.go b/provider/webhook/webhook.go index 5b875930a9..38573a7a9c 100644 --- a/provider/webhook/webhook.go +++ b/provider/webhook/webhook.go @@ -240,7 +240,7 @@ func (p WebhookProvider) AdjustEndpoints(e []*endpoint.Endpoint) ([]*endpoint.En if resp.StatusCode != http.StatusOK { adjustEndpointsErrorsGauge.Inc() log.Debugf("Failed to AdjustEndpoints with code %d", resp.StatusCode) - return nil, err + return nil, fmt.Errorf("failed to AdjustEndpoints with code %d", resp.StatusCode) } if err := json.NewDecoder(resp.Body).Decode(&endpoints); err != nil { diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index 011d66d65c..9c8d470eb3 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -46,17 +46,20 @@ func TestInvalidDomainFilter(t *testing.T) { } func TestValidDomainfilter(t *testing.T) { + // initialize domanin filter + domainFilter := endpoint.NewDomainFilter([]string{"example.com"}) svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) - w.Write([]byte(`{}`)) + json.NewEncoder(w).Encode(domainFilter) return } })) defer svr.Close() - _, err := NewWebhookProvider(svr.URL) + p, err := NewWebhookProvider(svr.URL) require.NoError(t, err) + require.Equal(t, p.GetDomainFilter(), endpoint.NewDomainFilter([]string{"example.com"})) } func TestRecords(t *testing.T) { @@ -66,6 +69,7 @@ func TestRecords(t *testing.T) { w.Write([]byte(`{}`)) return } + require.Equal(t, "/records", r.URL.Path) w.Write([]byte(`[{ "dnsName" : "test.example.com" }]`)) @@ -82,6 +86,24 @@ func TestRecords(t *testing.T) { }}, endpoints) } +func TestRecordsWithErrors(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.Write([]byte(`{}`)) + return + } + require.Equal(t, "/records", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer svr.Close() + + provider, err := NewWebhookProvider(svr.URL) + require.NoError(t, err) + _, err = provider.Records(context.Background()) + require.NotNil(t, err) +} + func TestApplyChanges(t *testing.T) { successfulApplyChanges := true svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -90,6 +112,7 @@ func TestApplyChanges(t *testing.T) { w.Write([]byte(`{}`)) return } + require.Equal(t, "/records", r.URL.Path) if successfulApplyChanges { w.WriteHeader(http.StatusNoContent) } else { @@ -116,6 +139,8 @@ func TestAdjustEndpoints(t *testing.T) { w.Write([]byte(`{}`)) return } + require.Equal(t, "/adjustendpoints", r.URL.Path) + var endpoints []*endpoint.Endpoint defer r.Body.Close() b, err := io.ReadAll(r.Body) @@ -158,5 +183,32 @@ func TestAdjustEndpoints(t *testing.T) { "", }, }}, adjustedEndpoints) +} + +func TestAdjustendpointsWithError(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/" { + w.Header().Set(contentTypeHeader, mediaTypeFormatAndVersion) + w.Write([]byte(`{}`)) + return + } + require.Equal(t, "/adjustendpoints", r.URL.Path) + w.WriteHeader(http.StatusInternalServerError) + })) + defer svr.Close() + provider, err := NewWebhookProvider(svr.URL) + require.NoError(t, err) + endpoints := []*endpoint.Endpoint{ + { + DNSName: "test.example.com", + RecordTTL: 10, + RecordType: "A", + Targets: endpoint.Targets{ + "", + }, + }, + } + _, err = provider.AdjustEndpoints(endpoints) + require.Error(t, err) } From 344679c238de58393511c04f81e9e1277132f1ed Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Fri, 22 Sep 2023 13:47:04 +0200 Subject: [PATCH 19/19] fix typo Signed-off-by: Raffaele Di Fazio --- provider/webhook/webhook_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/provider/webhook/webhook_test.go b/provider/webhook/webhook_test.go index 9c8d470eb3..7fbd0aa276 100644 --- a/provider/webhook/webhook_test.go +++ b/provider/webhook/webhook_test.go @@ -46,7 +46,7 @@ func TestInvalidDomainFilter(t *testing.T) { } func TestValidDomainfilter(t *testing.T) { - // initialize domanin filter + // initialize domain filter domainFilter := endpoint.NewDomainFilter([]string{"example.com"}) svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" {