From b4ff5d7fdc541c510e492dd294eb0885c437eaeb Mon Sep 17 00:00:00 2001 From: Zhiying Lin <54013513+zhiying-lin@users.noreply.github.com> Date: Wed, 18 Sep 2024 16:36:02 +0800 Subject: [PATCH] feat: add trafficManagerProfile controller (#193) --- go.mod | 21 +- go.sum | 46 ++- pkg/common/azureerrors/azureerrors.go | 39 +++ pkg/common/azureerrors/azureerrors_test.go | 161 +++++++++ pkg/common/objectmeta/objectmeta.go | 22 +- .../hub/membercluster/suite_test.go | 3 +- .../hub/trafficmanagerprofile/controller.go | 271 ++++++++++++++ .../controller_integration_test.go | 330 ++++++++++++++++++ .../trafficmanagerprofile/controller_test.go | 113 ++++++ .../hub/trafficmanagerprofile/suite_test.go | 140 ++++++++ .../v1alpha1/suite_test.go | 2 +- .../v1beta1/suite_test.go | 2 +- .../trafficmanager/fakeprovider/profile.go | 150 ++++++++ .../trafficmanager/validator/profile.go | 68 ++++ test/scripts/bootstrap.sh | 2 +- 15 files changed, 1338 insertions(+), 32 deletions(-) create mode 100644 pkg/common/azureerrors/azureerrors.go create mode 100644 pkg/common/azureerrors/azureerrors_test.go create mode 100644 pkg/controllers/hub/trafficmanagerprofile/controller.go create mode 100644 pkg/controllers/hub/trafficmanagerprofile/controller_integration_test.go create mode 100644 pkg/controllers/hub/trafficmanagerprofile/controller_test.go create mode 100644 pkg/controllers/hub/trafficmanagerprofile/suite_test.go create mode 100644 test/common/trafficmanager/fakeprovider/profile.go create mode 100644 test/common/trafficmanager/validator/profile.go diff --git a/go.mod b/go.mod index 8ffe32ac..90b7ccdc 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,15 @@ module go.goms.io/fleet-networking go 1.22.7 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 + github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.3.0 github.com/google/go-cmp v0.6.0 - github.com/onsi/ginkgo/v2 v2.17.2 - github.com/onsi/gomega v1.33.1 + github.com/onsi/ginkgo/v2 v2.19.1 + github.com/onsi/gomega v1.34.0 github.com/prometheus/client_golang v1.19.1 github.com/prometheus/common v0.55.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/sync v0.7.0 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 k8s.io/client-go v0.30.2 @@ -16,13 +20,10 @@ require ( sigs.k8s.io/controller-runtime v0.18.4 ) -require ( - github.com/stretchr/testify v1.9.0 - go.goms.io/fleet v0.10.5 - golang.org/x/sync v0.7.0 -) +require go.goms.io/fleet v0.10.10 require ( + github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -58,10 +59,10 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect diff --git a/go.sum b/go.sum index 3660d722..522759cb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,13 @@ +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.3.0 h1:e3kTG23M5ps+DjvPolK4dcgohDY8sHsXU7zrdHj1WzY= +github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager v1.3.0/go.mod h1:Os5dq8Cvvz97rJauZhZJAfKHN+OEvF/0nVmHzF4aVys= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -27,6 +37,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -57,6 +69,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -68,10 +82,12 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= -github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= -github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= +github.com/onsi/ginkgo/v2 v2.19.1 h1:QXgq3Z8Crl5EL1WBAC98A5sEBHARrAJNzAmMxzLcRF0= +github.com/onsi/ginkgo/v2 v2.19.1/go.mod h1:O3DtEWQkPa/F7fBMgmZQKKsluAy8pd3rEQdrjkPb9zA= +github.com/onsi/gomega v1.34.0 h1:eSSPsPNp6ZpsG8X1OVmOTxig+CblTc4AxpPBykhe2Os= +github.com/onsi/gomega v1.34.0/go.mod h1:MIKI8c+f+QLWk+hxbePD4i0LMJSExPaZOVfkoex4cAo= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -84,8 +100,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -94,8 +110,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.goms.io/fleet v0.10.5 h1:Zc+pLk77zWv0hAqBbFZEMMd05MVw9P8jp8YHTy7WPdI= -go.goms.io/fleet v0.10.5/go.mod h1:FpVP3YsiewmyGH77Yx6sLngHbZKgepnmJDIibz2pjZo= +go.goms.io/fleet v0.10.10 h1:qdOfSCEVKFmv5K1O5/iftj5DzlxyRYNsM3DGrSO0FwE= +go.goms.io/fleet v0.10.10/go.mod h1:WkN23NUb/efeo76BwFO5xxEwR6BMvq0nwl3/GeBdYRg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -105,6 +121,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -113,8 +131,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -125,10 +143,10 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= diff --git a/pkg/common/azureerrors/azureerrors.go b/pkg/common/azureerrors/azureerrors.go new file mode 100644 index 00000000..bb075067 --- /dev/null +++ b/pkg/common/azureerrors/azureerrors.go @@ -0,0 +1,39 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +// Package azureerrors defines shared azure error util functions. +package azureerrors + +import ( + "errors" + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" +) + +// IsNotFound returns true if the error is a http 404 error returned by the azure server. +func IsNotFound(err error) bool { + var responseError *azcore.ResponseError + return errors.As(err, &responseError) && responseError.StatusCode == http.StatusNotFound +} + +// IsClientError returns true if the error is a client error (400-499) returned by the azure server. +func IsClientError(err error) bool { + var responseError *azcore.ResponseError + return errors.As(err, &responseError) && + responseError.StatusCode >= http.StatusBadRequest && responseError.StatusCode < http.StatusInternalServerError +} + +// IsConflict determines if the error is a http 409 error returned by the azure server. +func IsConflict(err error) bool { + var responseError *azcore.ResponseError + return errors.As(err, &responseError) && responseError.StatusCode == http.StatusConflict +} + +// IsThrottled determines if the error is a http 429 error returned by the azure server. +func IsThrottled(err error) bool { + var responseError *azcore.ResponseError + return errors.As(err, &responseError) && responseError.StatusCode == http.StatusTooManyRequests +} diff --git a/pkg/common/azureerrors/azureerrors_test.go b/pkg/common/azureerrors/azureerrors_test.go new file mode 100644 index 00000000..c3231ed0 --- /dev/null +++ b/pkg/common/azureerrors/azureerrors_test.go @@ -0,0 +1,161 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package azureerrors + +import ( + "errors" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" +) + +func TestIsNotFound(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "nil error", + err: nil, + want: false, + }, + { + name: "not azure error", + err: errors.New("not azure error"), + want: false, + }, + { + name: "bad request error", + err: &azcore.ResponseError{StatusCode: 400}, + want: false, + }, + { + name: "not found error", + err: &azcore.ResponseError{StatusCode: 404}, + want: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := IsNotFound(tc.err) + if got != tc.want { + t.Errorf("IsNotFound() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestIsClientError(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "nil error", + err: nil, + want: false, + }, + { + name: "not azure error", + err: errors.New("not azure error"), + want: false, + }, + { + name: "bad request error", + err: &azcore.ResponseError{StatusCode: 400}, + want: true, + }, + { + name: "not found error", + err: &azcore.ResponseError{StatusCode: 404}, + want: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := IsClientError(tc.err) + if got != tc.want { + t.Errorf("IsClientError() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestIsConflict(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "nil error", + err: nil, + want: false, + }, + { + name: "not azure error", + err: errors.New("not azure error"), + want: false, + }, + { + name: "bad request error", + err: &azcore.ResponseError{StatusCode: 400}, + want: false, + }, + { + name: "conflict error", + err: &azcore.ResponseError{StatusCode: 409}, + want: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := IsConflict(tc.err) + if got != tc.want { + t.Errorf("IsConflict() = %v, want %v", got, tc.want) + } + }) + } +} + +func TestIsThrottled(t *testing.T) { + tests := []struct { + name string + err error + want bool + }{ + { + name: "nil error", + err: nil, + want: false, + }, + { + name: "not azure error", + err: errors.New("not azure error"), + want: false, + }, + { + name: "bad request error", + err: &azcore.ResponseError{StatusCode: 400}, + want: false, + }, + { + name: "throttled error", + err: &azcore.ResponseError{StatusCode: 429}, + want: true, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := IsThrottled(tc.err) + if got != tc.want { + t.Errorf("IsConflict() = %v, want %v", got, tc.want) + } + }) + } +} diff --git a/pkg/common/objectmeta/objectmeta.go b/pkg/common/objectmeta/objectmeta.go index 94a34d67..c40d59e8 100644 --- a/pkg/common/objectmeta/objectmeta.go +++ b/pkg/common/objectmeta/objectmeta.go @@ -6,28 +6,42 @@ Licensed under the MIT license. // Package objectmeta defines shared meta const used by the networking objects. package objectmeta +const ( + fleetNetworkingPrefix = "networking.fleet.azure.com/" +) + // Finalizers const ( // InternalServiceExportFinalizer is the finalizer InternalServiceExport controllers adds to mark that a // InternalServiceExport can only be deleted after both ServiceImport label and ServiceExport conflict resolution // result have been updated. - InternalServiceExportFinalizer = "networking.fleet.azure.com/internal-svc-export-cleanup" + InternalServiceExportFinalizer = fleetNetworkingPrefix + "internal-svc-export-cleanup" + + // TrafficManagerProfileFinalizer a finalizer added by the TrafficManagerProfile controller to all trafficManagerProfiles, + // to make sure that the controller can react to profile deletions if necessary. + TrafficManagerProfileFinalizer = fleetNetworkingPrefix + "traffic-manager-profile-cleanup" ) // Labels const ( // MultiClusterServiceLabelDerivedService is the label added by the MCS controller, which marks the // derived Service behind a MCS. - MultiClusterServiceLabelDerivedService = "networking.fleet.azure.com/derived-service" + MultiClusterServiceLabelDerivedService = fleetNetworkingPrefix + "derived-service" ) // Annotations const ( // ServiceImportAnnotationServiceInUseBy is the key of the ServiceInUseBy annotation, which marks the list // of member clusters importing an exported Service. - ServiceImportAnnotationServiceInUseBy = "networking.fleet.azure.com/service-in-use-by" + ServiceImportAnnotationServiceInUseBy = fleetNetworkingPrefix + "service-in-use-by" // ExportedObjectAnnotationUniqueName is an annotation that marks the fleet-scoped unique name assigned to // an exported object. - ExportedObjectAnnotationUniqueName = "networking.fleet.azure.com/fleet-unique-name" + ExportedObjectAnnotationUniqueName = fleetNetworkingPrefix + "fleet-unique-name" +) + +// Azure Resource Tags +const ( + // AzureTrafficManagerProfileTagKey is the key of the Azure Traffic Manager profile tag when the controller creates it. + AzureTrafficManagerProfileTagKey = fleetNetworkingPrefix + "trafficManagerProfile" ) diff --git a/pkg/controllers/hub/membercluster/suite_test.go b/pkg/controllers/hub/membercluster/suite_test.go index e0804372..1fec27d4 100644 --- a/pkg/controllers/hub/membercluster/suite_test.go +++ b/pkg/controllers/hub/membercluster/suite_test.go @@ -54,7 +54,8 @@ var _ = BeforeSuite(func() { hubTestEnv = &envtest.Environment{ CRDDirectoryPaths: []string{ filepath.Join("..", "..", "..", "..", "config", "crd", "bases"), - filepath.Join(build.Default.GOPATH, "pkg", "mod", "go.goms.io", "fleet@v0.10.5", "config", "crd", "bases", "cluster.kubernetes-fleet.io_memberclusters.yaml"), + // The package name must match with the version of the fleet package in use. + filepath.Join(build.Default.GOPATH, "pkg", "mod", "go.goms.io", "fleet@v0.10.10", "config", "crd", "bases", "cluster.kubernetes-fleet.io_memberclusters.yaml"), }, ErrorIfCRDPathMissing: true, } diff --git a/pkg/controllers/hub/trafficmanagerprofile/controller.go b/pkg/controllers/hub/trafficmanagerprofile/controller.go new file mode 100644 index 00000000..da64b53a --- /dev/null +++ b/pkg/controllers/hub/trafficmanagerprofile/controller.go @@ -0,0 +1,271 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +// Package trafficmanagerprofile features the TrafficManagerProfile controller to reconcile TrafficManagerProfile CRs. +package trafficmanagerprofile + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager" + "k8s.io/apimachinery/pkg/api/equality" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/klog/v2" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "go.goms.io/fleet/pkg/utils/controller" + + fleetnetv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" + "go.goms.io/fleet-networking/pkg/common/azureerrors" + "go.goms.io/fleet-networking/pkg/common/defaulter" + "go.goms.io/fleet-networking/pkg/common/objectmeta" +) + +const ( + // DNSRelativeNameFormat consists of "Profile-Namespace" and "Profile-Name". + DNSRelativeNameFormat = "%s-%s" + // AzureResourceProfileNameFormat is the name format of the Azure Traffic Manager Profile created by the fleet controller. + AzureResourceProfileNameFormat = "fleet-%s" +) + +var ( + // create the func as a variable so that the integration test can use a customized function. + generateAzureTrafficManagerProfileNameFunc = func(profile *fleetnetv1alpha1.TrafficManagerProfile) string { + return fmt.Sprintf(AzureResourceProfileNameFormat, profile.UID) + } +) + +// Reconciler reconciles a TrafficManagerProfile object. +type Reconciler struct { + client.Client + + ProfilesClient *armtrafficmanager.ProfilesClient + ResourceGroupName string // default resource group name to create azure traffic manager profiles +} + +//+kubebuilder:rbac:groups=networking.fleet.azure.com,resources=trafficmanagerprofiles,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=networking.fleet.azure.com,resources=trafficmanagerprofiles/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=networking.fleet.azure.com,resources=trafficmanagerprofiles/finalizers,verbs=get;update +//+kubebuilder:rbac:groups="",resources=events,verbs=create;patch + +// Reconcile triggers a single reconcile round. +func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + name := req.NamespacedName + profileKRef := klog.KRef(name.Namespace, name.Name) + + startTime := time.Now() + klog.V(2).InfoS("Reconciliation starts", "trafficManagerProfile", profileKRef) + defer func() { + latency := time.Since(startTime).Milliseconds() + klog.V(2).InfoS("Reconciliation ends", "trafficManagerProfile", profileKRef, "latency", latency) + }() + + profile := &fleetnetv1alpha1.TrafficManagerProfile{} + if err := r.Client.Get(ctx, name, profile); err != nil { + if apierrors.IsNotFound(err) { + klog.V(4).InfoS("Ignoring NotFound trafficManagerProfile", "trafficManagerProfile", profileKRef) + return ctrl.Result{}, nil + } + klog.ErrorS(err, "Failed to get trafficManagerProfile", "trafficManagerProfile", profileKRef) + return ctrl.Result{}, controller.NewAPIServerError(true, err) + } + + if !profile.ObjectMeta.DeletionTimestamp.IsZero() { + // TODO: handle the deletion when backends are still attached to the profile + return r.handleDelete(ctx, profile) + } + + // register finalizer + if !controllerutil.ContainsFinalizer(profile, objectmeta.TrafficManagerProfileFinalizer) { + controllerutil.AddFinalizer(profile, objectmeta.TrafficManagerProfileFinalizer) + if err := r.Update(ctx, profile); err != nil { + klog.ErrorS(err, "Failed to add finalizer to trafficManagerProfile", "trafficManagerProfile", profileKRef) + return ctrl.Result{}, controller.NewUpdateIgnoreConflictError(err) + } + } + + // TODO: replace the following with defaulter wehbook + defaulter.SetDefaultsTrafficManagerProfile(profile) + return r.handleUpdate(ctx, profile) +} + +func (r *Reconciler) handleDelete(ctx context.Context, profile *fleetnetv1alpha1.TrafficManagerProfile) (ctrl.Result, error) { + profileKObj := klog.KObj(profile) + // The profile is being deleted + if !controllerutil.ContainsFinalizer(profile, objectmeta.TrafficManagerProfileFinalizer) { + klog.V(4).InfoS("TrafficManagerProfile is being deleted", "trafficManagerProfile", profileKObj) + return ctrl.Result{}, nil + } + + azureProfileName := generateAzureTrafficManagerProfileNameFunc(profile) + klog.V(2).InfoS("Deleting Azure Traffic Manager profile", "trafficManagerProfile", profileKObj, "azureProfileName", azureProfileName) + if _, err := r.ProfilesClient.Delete(ctx, r.ResourceGroupName, azureProfileName, nil); err != nil { + if !azureerrors.IsNotFound(err) { + klog.ErrorS(err, "Failed to delete Azure Traffic Manager profile", "trafficManagerProfile", profileKObj, "azureProfileName", azureProfileName) + return ctrl.Result{}, err + } + } + + controllerutil.RemoveFinalizer(profile, objectmeta.TrafficManagerProfileFinalizer) + if err := r.Client.Update(ctx, profile); err != nil { + klog.ErrorS(err, "Failed to remove trafficManagerProfile finalizer", "trafficManagerProfile", profileKObj) + return ctrl.Result{}, err + } + klog.V(2).InfoS("Removed trafficManagerProfile finalizer", "trafficManagerProfile", profileKObj) + return ctrl.Result{}, nil +} + +func (r *Reconciler) handleUpdate(ctx context.Context, profile *fleetnetv1alpha1.TrafficManagerProfile) (ctrl.Result, error) { + profileKObj := klog.KObj(profile) + azureProfileName := generateAzureTrafficManagerProfileNameFunc(profile) + var responseError *azcore.ResponseError + getRes, getErr := r.ProfilesClient.Get(ctx, r.ResourceGroupName, azureProfileName, nil) + if getErr != nil { + if !azureerrors.IsNotFound(getErr) { + klog.ErrorS(getErr, "Failed to get the profile", "trafficManagerProfile", profileKObj, "azureProfileName", azureProfileName) + return ctrl.Result{}, getErr + } + klog.V(2).InfoS("Azure Traffic Manager profile does not exist", "trafficManagerProfile", profileKObj, "azureProfileName", azureProfileName) + } else { + existingSpec := convertToTrafficManagerProfileSpec(&getRes.Profile) + if equality.Semantic.DeepEqual(existingSpec, profile.Spec) { + // skip creating or updating the profile + klog.V(2).InfoS("No profile update needed", "trafficManagerProfile", profileKObj, "azureProfileName", azureProfileName) + return r.updateProfileStatus(ctx, profile, getRes.Profile, nil) + } + } + + res, updateErr := r.ProfilesClient.CreateOrUpdate(ctx, r.ResourceGroupName, azureProfileName, generateAzureTrafficManagerProfile(profile), nil) + if updateErr != nil { + if !errors.As(updateErr, &responseError) { + klog.ErrorS(updateErr, "Failed to send the createOrUpdate request", "trafficManagerProfile", profileKObj, "azureProfileName", azureProfileName) + return ctrl.Result{}, updateErr + } + klog.ErrorS(updateErr, "Failed to create or update a profile", "trafficManagerProfile", profileKObj, + "azureProfileName", azureProfileName, + "errorCode", responseError.ErrorCode, "statusCode", responseError.StatusCode) + } + return r.updateProfileStatus(ctx, profile, res.Profile, updateErr) +} + +func convertToTrafficManagerProfileSpec(profile *armtrafficmanager.Profile) fleetnetv1alpha1.TrafficManagerProfileSpec { + if profile.Properties != nil && profile.Properties.MonitorConfig != nil { + var protocol fleetnetv1alpha1.TrafficManagerMonitorProtocol + if profile.Properties.MonitorConfig.Protocol != nil { + protocol = fleetnetv1alpha1.TrafficManagerMonitorProtocol(*profile.Properties.MonitorConfig.Protocol) + } + return fleetnetv1alpha1.TrafficManagerProfileSpec{ + MonitorConfig: &fleetnetv1alpha1.MonitorConfig{ + IntervalInSeconds: profile.Properties.MonitorConfig.IntervalInSeconds, + Path: profile.Properties.MonitorConfig.Path, + Port: profile.Properties.MonitorConfig.Port, + Protocol: &protocol, + TimeoutInSeconds: profile.Properties.MonitorConfig.TimeoutInSeconds, + ToleratedNumberOfFailures: profile.Properties.MonitorConfig.ToleratedNumberOfFailures, + }, + } + } + return fleetnetv1alpha1.TrafficManagerProfileSpec{} +} + +func (r *Reconciler) updateProfileStatus(ctx context.Context, profile *fleetnetv1alpha1.TrafficManagerProfile, azureProfile armtrafficmanager.Profile, updateErr error) (ctrl.Result, error) { + profileKObj := klog.KObj(profile) + if updateErr == nil { + // azureProfile.Properties.DNSConfig.Fqdn should not be nil + if azureProfile.Properties != nil && azureProfile.Properties.DNSConfig != nil { + profile.Status.DNSName = azureProfile.Properties.DNSConfig.Fqdn + } else { + err := fmt.Errorf("got nil DNSConfig for Azure Traffic Manager profile") + klog.ErrorS(controller.NewUnexpectedBehaviorError(err), "Unexpected value returned by the Azure Traffic Manager", "trafficManagerProfile", profileKObj, "azureProfileName", azureProfile.Name) + profile.Status.DNSName = nil // reset the DNS name + } + } else { + profile.Status.DNSName = nil // reset the DNS name + } + + cond := metav1.Condition{ + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Status: metav1.ConditionTrue, + ObservedGeneration: profile.Generation, + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonProgrammed), + Message: "Successfully configured the Azure Traffic Manager profile", + } + if azureerrors.IsConflict(updateErr) { + cond = metav1.Condition{ + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Status: metav1.ConditionFalse, + ObservedGeneration: profile.Generation, + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonDNSNameNotAvailable), + Message: "Domain name is not available. Please choose a different profile name or namespace", + } + } else if azureerrors.IsClientError(updateErr) && !azureerrors.IsThrottled(updateErr) { + cond = metav1.Condition{ + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Status: metav1.ConditionFalse, + ObservedGeneration: profile.Generation, + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonInvalid), + Message: fmt.Sprintf("Invalid profile: %v", updateErr), + } + } else if updateErr != nil { + cond = metav1.Condition{ + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Status: metav1.ConditionFalse, + ObservedGeneration: profile.Generation, + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonPending), + Message: fmt.Sprintf("Failed to configure profile and retyring: %v", updateErr), + } + } + meta.SetStatusCondition(&profile.Status.Conditions, cond) + if err := r.Client.Status().Update(ctx, profile); err != nil { + klog.ErrorS(err, "Failed to update trafficManagerProfile status", "trafficManagerProfile", profileKObj) + return ctrl.Result{}, controller.NewUpdateIgnoreConflictError(err) + } + klog.V(2).InfoS("Updated the trafficProfile status", "trafficManagerProfile", profileKObj, "status", profile.Status) + return ctrl.Result{}, updateErr +} + +func generateAzureTrafficManagerProfile(profile *fleetnetv1alpha1.TrafficManagerProfile) armtrafficmanager.Profile { + mc := profile.Spec.MonitorConfig + namespacedName := types.NamespacedName{Name: profile.Name, Namespace: profile.Namespace} + return armtrafficmanager.Profile{ + Location: ptr.To("global"), + Properties: &armtrafficmanager.ProfileProperties{ + DNSConfig: &armtrafficmanager.DNSConfig{ + RelativeName: ptr.To(fmt.Sprintf(DNSRelativeNameFormat, profile.Namespace, profile.Name)), + }, + MonitorConfig: &armtrafficmanager.MonitorConfig{ + IntervalInSeconds: mc.IntervalInSeconds, + Path: mc.Path, + Port: mc.Port, + Protocol: ptr.To(armtrafficmanager.MonitorProtocol(*mc.Protocol)), + TimeoutInSeconds: mc.TimeoutInSeconds, + ToleratedNumberOfFailures: mc.ToleratedNumberOfFailures, + }, + ProfileStatus: ptr.To(armtrafficmanager.ProfileStatusEnabled), + // By default, the routing method is set to Weighted. + TrafficRoutingMethod: ptr.To(armtrafficmanager.TrafficRoutingMethodWeighted), + }, + Tags: map[string]*string{ + objectmeta.AzureTrafficManagerProfileTagKey: ptr.To(namespacedName.String()), + }, + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&fleetnetv1alpha1.TrafficManagerProfile{}). + Complete(r) +} diff --git a/pkg/controllers/hub/trafficmanagerprofile/controller_integration_test.go b/pkg/controllers/hub/trafficmanagerprofile/controller_integration_test.go new file mode 100644 index 00000000..eb1280ae --- /dev/null +++ b/pkg/controllers/hub/trafficmanagerprofile/controller_integration_test.go @@ -0,0 +1,330 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package trafficmanagerprofile + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" + + fleetnetv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" + "go.goms.io/fleet-networking/pkg/common/objectmeta" + "go.goms.io/fleet-networking/test/common/trafficmanager/fakeprovider" + "go.goms.io/fleet-networking/test/common/trafficmanager/validator" +) + +func trafficManagerProfileForTest(name string) *fleetnetv1alpha1.TrafficManagerProfile { + return &fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + }, + Spec: fleetnetv1alpha1.TrafficManagerProfileSpec{ + MonitorConfig: &fleetnetv1alpha1.MonitorConfig{ + IntervalInSeconds: ptr.To[int64](30), + Path: ptr.To("/healthz"), + Port: ptr.To[int64](8080), + Protocol: ptr.To(fleetnetv1alpha1.TrafficManagerMonitorProtocolHTTPS), + TimeoutInSeconds: ptr.To[int64](10), + ToleratedNumberOfFailures: ptr.To[int64](5), + }, + }, + } +} + +var _ = Describe("Test TrafficManagerProfile Controller", func() { + Context("When updating existing valid trafficManagerProfile", Ordered, func() { + name := fakeprovider.ValidProfileName + var profile *fleetnetv1alpha1.TrafficManagerProfile + relativeDNSName := fmt.Sprintf(DNSRelativeNameFormat, testNamespace, name) + fqdn := fmt.Sprintf(fakeprovider.ProfileDNSNameFormat, relativeDNSName) + + It("AzureTrafficManager should be configured", func() { + By("By creating a new TrafficManagerProfile") + profile = trafficManagerProfileForTest(name) + Expect(k8sClient.Create(ctx, profile)).Should(Succeed()) + + By("By checking profile") + want := fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + Finalizers: []string{objectmeta.TrafficManagerProfileFinalizer}, + }, + Spec: profile.Spec, + Status: fleetnetv1alpha1.TrafficManagerProfileStatus{ + DNSName: ptr.To(fqdn), + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonProgrammed), + }, + }, + }, + } + validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want) + }) + + It("Update the trafficManagerProfile spec", func() { + Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: testNamespace, Name: name}, profile)).Should(Succeed(), "failed to get the trafficManagerProfile") + profile.Spec.MonitorConfig.IntervalInSeconds = ptr.To[int64](10) + profile.Spec.MonitorConfig.TimeoutInSeconds = ptr.To[int64](10) + Expect(k8sClient.Update(ctx, profile)).Should(Succeed(), "failed to update the trafficManagerProfile") + }) + + It("Validating trafficManagerProfile status and update should fail", func() { + want := fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + Finalizers: []string{objectmeta.TrafficManagerProfileFinalizer}, + }, + Spec: profile.Spec, + Status: fleetnetv1alpha1.TrafficManagerProfileStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonInvalid), + }, + }, + }, + } + validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want) + }) + + It("Deleting trafficManagerProfile", func() { + err := k8sClient.Delete(ctx, profile) + Expect(err).Should(Succeed(), "failed to delete trafficManagerProfile") + }) + + It("Validating trafficManagerProfile is deleted", func() { + validator.IsTrafficManagerProfileDeleted(ctx, k8sClient, types.NamespacedName{Namespace: testNamespace, Name: name}) + }) + }) + + Context("When updating existing valid trafficManagerProfile with no changes", Ordered, func() { + name := fakeprovider.ValidProfileName + var profile *fleetnetv1alpha1.TrafficManagerProfile + + It("AzureTrafficManager should be configured", func() { + By("By creating a new TrafficManagerProfile") + profile = &fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + }, + Spec: fleetnetv1alpha1.TrafficManagerProfileSpec{ + MonitorConfig: &fleetnetv1alpha1.MonitorConfig{ + IntervalInSeconds: ptr.To[int64](10), + Path: ptr.To("/healthz"), + Port: ptr.To[int64](8080), + Protocol: ptr.To(fleetnetv1alpha1.TrafficManagerMonitorProtocolHTTP), + TimeoutInSeconds: ptr.To[int64](9), + ToleratedNumberOfFailures: ptr.To[int64](4), + }, + }, + } + Expect(k8sClient.Create(ctx, profile)).Should(Succeed()) + + By("By checking profile") + want := fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + Finalizers: []string{objectmeta.TrafficManagerProfileFinalizer}, + }, + Spec: profile.Spec, + Status: fleetnetv1alpha1.TrafficManagerProfileStatus{ + // The DNS name is returned by the fake Azure GET call. + DNSName: ptr.To(fmt.Sprintf(fakeprovider.ProfileDNSNameFormat, name)), + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionTrue, + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonProgrammed), + }, + }, + }, + } + validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want) + }) + + It("Deleting trafficManagerProfile", func() { + err := k8sClient.Delete(ctx, profile) + Expect(err).Should(Succeed(), "failed to delete trafficManagerProfile") + }) + + It("Validating trafficManagerProfile is deleted", func() { + validator.IsTrafficManagerProfileDeleted(ctx, k8sClient, types.NamespacedName{Namespace: testNamespace, Name: name}) + }) + }) + + Context("When creating trafficManagerProfile and DNS name is not available", Ordered, func() { + name := fakeprovider.ConflictErrProfileName + var profile *fleetnetv1alpha1.TrafficManagerProfile + + It("AzureTrafficManager should not be configured", func() { + By("By creating a new TrafficManagerProfile") + profile = trafficManagerProfileForTest(name) + Expect(k8sClient.Create(ctx, profile)).Should(Succeed()) + + By("By checking profile") + want := fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + Finalizers: []string{objectmeta.TrafficManagerProfileFinalizer}, + }, + Spec: profile.Spec, + Status: fleetnetv1alpha1.TrafficManagerProfileStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonDNSNameNotAvailable), + }, + }, + }, + } + validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want) + }) + + It("Deleting trafficManagerProfile", func() { + err := k8sClient.Delete(ctx, profile) + Expect(err).Should(Succeed(), "failed to delete trafficManagerProfile") + }) + + It("Validating trafficManagerProfile is deleted", func() { + validator.IsTrafficManagerProfileDeleted(ctx, k8sClient, types.NamespacedName{Namespace: testNamespace, Name: name}) + }) + }) + + Context("When creating trafficManagerProfile and azure request failed because of too many requests", Ordered, func() { + name := fakeprovider.ThrottledErrProfileName + var profile *fleetnetv1alpha1.TrafficManagerProfile + + It("AzureTrafficManager should not be configured", func() { + By("By creating a new TrafficManagerProfile") + profile = trafficManagerProfileForTest(name) + Expect(k8sClient.Create(ctx, profile)).Should(Succeed()) + + By("By checking profile") + want := fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + Finalizers: []string{objectmeta.TrafficManagerProfileFinalizer}, + }, + Spec: profile.Spec, + Status: fleetnetv1alpha1.TrafficManagerProfileStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonPending), + }, + }, + }, + } + validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want) + }) + + It("Deleting trafficManagerProfile", func() { + err := k8sClient.Delete(ctx, profile) + Expect(err).Should(Succeed(), "failed to delete trafficManagerProfile") + }) + + It("Validating trafficManagerProfile is deleted", func() { + validator.IsTrafficManagerProfileDeleted(ctx, k8sClient, types.NamespacedName{Namespace: testNamespace, Name: name}) + }) + }) + + Context("When creating trafficManagerProfile and azure request failed because of client side error", Ordered, func() { + name := "bad-request" + var profile *fleetnetv1alpha1.TrafficManagerProfile + + It("AzureTrafficManager should not be configured", func() { + By("By creating a new TrafficManagerProfile") + profile = trafficManagerProfileForTest(name) + Expect(k8sClient.Create(ctx, profile)).Should(Succeed()) + + By("By checking profile") + want := fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + Finalizers: []string{objectmeta.TrafficManagerProfileFinalizer}, + }, + Spec: profile.Spec, + Status: fleetnetv1alpha1.TrafficManagerProfileStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonInvalid), + }, + }, + }, + } + validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want) + }) + + It("Deleting trafficManagerProfile", func() { + err := k8sClient.Delete(ctx, profile) + Expect(err).Should(Succeed(), "failed to delete trafficManagerProfile") + }) + + It("Validating trafficManagerProfile is deleted", func() { + validator.IsTrafficManagerProfileDeleted(ctx, k8sClient, types.NamespacedName{Namespace: testNamespace, Name: name}) + }) + }) + + Context("When creating trafficManagerProfile and azure request failed because of internal server error", Ordered, func() { + name := fakeprovider.InternalServerErrProfileName + var profile *fleetnetv1alpha1.TrafficManagerProfile + + It("AzureTrafficManager should not be configured", func() { + By("By creating a new TrafficManagerProfile") + profile = trafficManagerProfileForTest(name) + Expect(k8sClient.Create(ctx, profile)).Should(Succeed()) + + By("By checking profile") + want := fleetnetv1alpha1.TrafficManagerProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testNamespace, + Finalizers: []string{objectmeta.TrafficManagerProfileFinalizer}, + }, + Spec: profile.Spec, + Status: fleetnetv1alpha1.TrafficManagerProfileStatus{ + Conditions: []metav1.Condition{ + { + Status: metav1.ConditionFalse, + Type: string(fleetnetv1alpha1.TrafficManagerProfileConditionProgrammed), + Reason: string(fleetnetv1alpha1.TrafficManagerProfileReasonPending), + }, + }, + }, + } + validator.ValidateTrafficManagerProfile(ctx, k8sClient, &want) + }) + + It("Deleting trafficManagerProfile", func() { + err := k8sClient.Delete(ctx, profile) + Expect(err).Should(Succeed(), "failed to delete trafficManagerProfile") + }) + + It("Validating trafficManagerProfile is deleted", func() { + validator.IsTrafficManagerProfileDeleted(ctx, k8sClient, types.NamespacedName{Namespace: testNamespace, Name: name}) + }) + }) +}) diff --git a/pkg/controllers/hub/trafficmanagerprofile/controller_test.go b/pkg/controllers/hub/trafficmanagerprofile/controller_test.go new file mode 100644 index 00000000..bec9ebcb --- /dev/null +++ b/pkg/controllers/hub/trafficmanagerprofile/controller_test.go @@ -0,0 +1,113 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package trafficmanagerprofile + +import ( + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager" + "github.com/google/go-cmp/cmp" + "k8s.io/utils/ptr" + + fleetnetv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" +) + +func TestConvertToTrafficManagerProfileSpec(t *testing.T) { + tests := []struct { + name string + profile *armtrafficmanager.Profile + want fleetnetv1alpha1.TrafficManagerProfileSpec + }{ + { + name: "nil properties", // not possible in production + profile: &armtrafficmanager.Profile{}, + want: fleetnetv1alpha1.TrafficManagerProfileSpec{}, + }, + { + name: "nil monitor protocol", // not possible in production + profile: &armtrafficmanager.Profile{ + Properties: &armtrafficmanager.ProfileProperties{ + MonitorConfig: &armtrafficmanager.MonitorConfig{ + IntervalInSeconds: ptr.To(int64(10)), + Path: ptr.To("/healthz"), + Port: ptr.To(int64(8080)), + TimeoutInSeconds: ptr.To(int64(9)), + ToleratedNumberOfFailures: ptr.To(int64(4)), + }, + }, + }, + want: fleetnetv1alpha1.TrafficManagerProfileSpec{ + MonitorConfig: &fleetnetv1alpha1.MonitorConfig{ + IntervalInSeconds: ptr.To(int64(10)), + Path: ptr.To("/healthz"), + Port: ptr.To(int64(8080)), + Protocol: (*fleetnetv1alpha1.TrafficManagerMonitorProtocol)(ptr.To("")), + TimeoutInSeconds: ptr.To(int64(9)), + ToleratedNumberOfFailures: ptr.To(int64(4)), + }, + }, + }, + { + name: "invalid monitor protocol", // not possible in production + profile: &armtrafficmanager.Profile{ + Properties: &armtrafficmanager.ProfileProperties{ + MonitorConfig: &armtrafficmanager.MonitorConfig{ + IntervalInSeconds: ptr.To(int64(10)), + Path: ptr.To("/healthz"), + Port: ptr.To(int64(8080)), + Protocol: (*armtrafficmanager.MonitorProtocol)(ptr.To("UDP")), + TimeoutInSeconds: ptr.To(int64(9)), + ToleratedNumberOfFailures: ptr.To(int64(4)), + }, + }, + }, + want: fleetnetv1alpha1.TrafficManagerProfileSpec{ + MonitorConfig: &fleetnetv1alpha1.MonitorConfig{ + IntervalInSeconds: ptr.To(int64(10)), + Path: ptr.To("/healthz"), + Port: ptr.To(int64(8080)), + Protocol: (*fleetnetv1alpha1.TrafficManagerMonitorProtocol)(ptr.To("UDP")), + TimeoutInSeconds: ptr.To(int64(9)), + ToleratedNumberOfFailures: ptr.To(int64(4)), + }, + }, + }, + { + name: "valid profile", // not possible in production + profile: &armtrafficmanager.Profile{ + Properties: &armtrafficmanager.ProfileProperties{ + MonitorConfig: &armtrafficmanager.MonitorConfig{ + IntervalInSeconds: ptr.To(int64(10)), + Path: ptr.To("/healthz"), + Port: ptr.To(int64(8080)), + Protocol: ptr.To(armtrafficmanager.MonitorProtocolHTTP), + TimeoutInSeconds: ptr.To(int64(9)), + ToleratedNumberOfFailures: ptr.To(int64(4)), + }, + }, + }, + want: fleetnetv1alpha1.TrafficManagerProfileSpec{ + MonitorConfig: &fleetnetv1alpha1.MonitorConfig{ + IntervalInSeconds: ptr.To(int64(10)), + Path: ptr.To("/healthz"), + Port: ptr.To(int64(8080)), + Protocol: ptr.To(fleetnetv1alpha1.TrafficManagerMonitorProtocolHTTP), + TimeoutInSeconds: ptr.To(int64(9)), + ToleratedNumberOfFailures: ptr.To(int64(4)), + }, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got := convertToTrafficManagerProfileSpec(tc.profile) + if diff := cmp.Diff(tc.want, got); diff != "" { + t.Errorf("convertToTrafficManagerProfileSpec() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/pkg/controllers/hub/trafficmanagerprofile/suite_test.go b/pkg/controllers/hub/trafficmanagerprofile/suite_test.go new file mode 100644 index 00000000..d9a9d68a --- /dev/null +++ b/pkg/controllers/hub/trafficmanagerprofile/suite_test.go @@ -0,0 +1,140 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package trafficmanagerprofile + +import ( + "context" + "flag" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/klog/v2" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + fleetnetv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" + "go.goms.io/fleet-networking/test/common/trafficmanager/fakeprovider" +) + +var ( + cfg *rest.Config + mgr manager.Manager + k8sClient client.Client + testEnv *envtest.Environment + ctx context.Context + cancel context.CancelFunc +) + +const ( + testNamespace = "profile-ns" +) + +var ( + originalGenerateAzureTrafficManagerProfileNameFunc = generateAzureTrafficManagerProfileNameFunc +) + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "TrafficManagerProfile Controller Suite") +} + +var _ = BeforeSuite(func() { + klog.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("../../../../", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = fleetnetv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + By("construct the k8s client") + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + By("starting the controller manager") + klog.InitFlags(flag.CommandLine) + flag.Parse() + + mgr, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + }) + Expect(err).NotTo(HaveOccurred()) + + profileClient, err := fakeprovider.NewProfileClient(ctx, "default-sub") + Expect(err).Should(Succeed(), "failed to create the fake profile client") + + generateAzureTrafficManagerProfileNameFunc = func(profile *fleetnetv1alpha1.TrafficManagerProfile) string { + return profile.Name + } + + err = (&Reconciler{ + Client: mgr.GetClient(), + ProfilesClient: profileClient, + ResourceGroupName: fakeprovider.DefaultResourceGroupName, + }).SetupWithManager(mgr) + Expect(err).ToNot(HaveOccurred()) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("Create profile namespace") + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + } + Expect(k8sClient.Create(ctx, &ns)).Should(Succeed()) + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + defer klog.Flush() + + By("delete profile namespace") + ns := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: testNamespace, + }, + } + Expect(k8sClient.Delete(ctx, &ns)).Should(Succeed()) + + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) + + generateAzureTrafficManagerProfileNameFunc = originalGenerateAzureTrafficManagerProfileNameFunc +}) diff --git a/pkg/controllers/member/internalmembercluster/v1alpha1/suite_test.go b/pkg/controllers/member/internalmembercluster/v1alpha1/suite_test.go index 5dba3352..9cbd4a93 100644 --- a/pkg/controllers/member/internalmembercluster/v1alpha1/suite_test.go +++ b/pkg/controllers/member/internalmembercluster/v1alpha1/suite_test.go @@ -83,7 +83,7 @@ var _ = BeforeSuite(func() { filepath.Join("../../../../../", "config", "crd", "bases"), // need to make sure the version matches the one in the go.mod // workaround mentioned in https://github.com/kubernetes-sigs/controller-runtime/issues/1191 - filepath.Join(build.Default.GOPATH, "pkg", "mod", "go.goms.io", "fleet@v0.10.5", "config", "crd", "bases"), + filepath.Join(build.Default.GOPATH, "pkg", "mod", "go.goms.io", "fleet@v0.10.10", "config", "crd", "bases"), }, ErrorIfCRDPathMissing: true, } diff --git a/pkg/controllers/member/internalmembercluster/v1beta1/suite_test.go b/pkg/controllers/member/internalmembercluster/v1beta1/suite_test.go index 0c32f679..18412189 100644 --- a/pkg/controllers/member/internalmembercluster/v1beta1/suite_test.go +++ b/pkg/controllers/member/internalmembercluster/v1beta1/suite_test.go @@ -80,7 +80,7 @@ var _ = BeforeSuite(func() { CRDDirectoryPaths: []string{ filepath.Join("..", "..", "..", "..", "..", "config", "crd", "bases"), // The package name must match with the version of the fleet package in use. - filepath.Join(build.Default.GOPATH, "pkg", "mod", "go.goms.io", "fleet@v0.10.5", "config", "crd", "bases"), + filepath.Join(build.Default.GOPATH, "pkg", "mod", "go.goms.io", "fleet@v0.10.10", "config", "crd", "bases"), }, ErrorIfCRDPathMissing: true, diff --git a/test/common/trafficmanager/fakeprovider/profile.go b/test/common/trafficmanager/fakeprovider/profile.go new file mode 100644 index 00000000..4a2a8b2b --- /dev/null +++ b/test/common/trafficmanager/fakeprovider/profile.go @@ -0,0 +1,150 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +// Package fakeprovider provides a fake azure implementation of traffic manager resources. +package fakeprovider + +import ( + "context" + "fmt" + "net/http" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager/fake" + "k8s.io/utils/ptr" + + azcorefake "github.com/Azure/azure-sdk-for-go/sdk/azcore/fake" + "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/trafficmanager/armtrafficmanager" +) + +const ( + DefaultResourceGroupName = "default-resource-group-name" + + ValidProfileName = "valid-profile" + ConflictErrProfileName = "conflict-err-profile" + InternalServerErrProfileName = "internal-server-err-profile" + ThrottledErrProfileName = "throttled-err-profile" + + ProfileDNSNameFormat = "%s.trafficmanager.net" +) + +// NewProfileClient creates a client which talks to a fake profile server. +func NewProfileClient(_ context.Context, subscriptionID string) (*armtrafficmanager.ProfilesClient, error) { + fakeServer := fake.ProfilesServer{ + CreateOrUpdate: CreateOrUpdate, + Delete: Delete, + Get: Get, + } + clientFactory, err := armtrafficmanager.NewClientFactory(subscriptionID, &azcorefake.TokenCredential{}, + &arm.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Transport: fake.NewProfilesServerTransport(&fakeServer), + }, + }) + if err != nil { + return nil, err + } + return clientFactory.NewProfilesClient(), nil +} + +// Get returns the http status code based on the profileName. +func Get(_ context.Context, resourceGroupName string, profileName string, _ *armtrafficmanager.ProfilesClientGetOptions) (resp azcorefake.Responder[armtrafficmanager.ProfilesClientGetResponse], errResp azcorefake.ErrorResponder) { + if resourceGroupName != DefaultResourceGroupName { + errResp.SetResponseError(http.StatusNotFound, "ResourceGroupNotFound") + return resp, errResp + } + switch profileName { + case ValidProfileName: + profileResp := armtrafficmanager.ProfilesClientGetResponse{ + Profile: armtrafficmanager.Profile{ + Name: ptr.To(profileName), + Location: ptr.To("global"), + Properties: &armtrafficmanager.ProfileProperties{ + DNSConfig: &armtrafficmanager.DNSConfig{ + Fqdn: ptr.To(fmt.Sprintf(ProfileDNSNameFormat, profileName)), + RelativeName: ptr.To(profileName), + TTL: ptr.To[int64](30), + }, + Endpoints: []*armtrafficmanager.Endpoint{}, + MonitorConfig: &armtrafficmanager.MonitorConfig{ + IntervalInSeconds: ptr.To(int64(10)), + Path: ptr.To("/healthz"), + Port: ptr.To(int64(8080)), + Protocol: ptr.To(armtrafficmanager.MonitorProtocolHTTP), + TimeoutInSeconds: ptr.To(int64(9)), + ToleratedNumberOfFailures: ptr.To(int64(4)), + }, + ProfileStatus: ptr.To(armtrafficmanager.ProfileStatusEnabled), + TrafficRoutingMethod: ptr.To(armtrafficmanager.TrafficRoutingMethodWeighted), + TrafficViewEnrollmentStatus: ptr.To(armtrafficmanager.TrafficViewEnrollmentStatusDisabled), + }, + }} + resp.SetResponse(http.StatusOK, profileResp, nil) + default: + errResp.SetResponseError(http.StatusNotFound, "NotFoundError") + } + return resp, errResp +} + +// CreateOrUpdate returns the http status code based on the profileName. +func CreateOrUpdate(_ context.Context, resourceGroupName string, profileName string, parameters armtrafficmanager.Profile, _ *armtrafficmanager.ProfilesClientCreateOrUpdateOptions) (resp azcorefake.Responder[armtrafficmanager.ProfilesClientCreateOrUpdateResponse], errResp azcorefake.ErrorResponder) { + if resourceGroupName != DefaultResourceGroupName { + errResp.SetResponseError(http.StatusNotFound, "ResourceGroupNotFound") + return resp, errResp + } + switch profileName { + case ConflictErrProfileName: + errResp.SetResponseError(http.StatusConflict, "Conflict") + case InternalServerErrProfileName: + errResp.SetResponseError(http.StatusInternalServerError, "InternalServerError") + case ThrottledErrProfileName: + errResp.SetResponseError(http.StatusTooManyRequests, "ThrottledError") + case ValidProfileName: + if parameters.Properties.MonitorConfig.IntervalInSeconds != nil && *parameters.Properties.MonitorConfig.IntervalInSeconds == 10 { + if parameters.Properties.MonitorConfig.TimeoutInSeconds != nil && *parameters.Properties.MonitorConfig.TimeoutInSeconds > 9 { + errResp.SetResponseError(http.StatusBadRequest, "BadRequestError") + return + } + } + profileResp := armtrafficmanager.ProfilesClientCreateOrUpdateResponse{ + Profile: armtrafficmanager.Profile{ + Name: ptr.To(profileName), + Location: ptr.To("global"), + Properties: &armtrafficmanager.ProfileProperties{ + DNSConfig: &armtrafficmanager.DNSConfig{ + Fqdn: ptr.To(fmt.Sprintf(ProfileDNSNameFormat, *parameters.Properties.DNSConfig.RelativeName)), + RelativeName: parameters.Properties.DNSConfig.RelativeName, + TTL: ptr.To[int64](30), + }, + Endpoints: []*armtrafficmanager.Endpoint{}, + MonitorConfig: parameters.Properties.MonitorConfig, + ProfileStatus: ptr.To(armtrafficmanager.ProfileStatusEnabled), + TrafficRoutingMethod: ptr.To(armtrafficmanager.TrafficRoutingMethodWeighted), + TrafficViewEnrollmentStatus: ptr.To(armtrafficmanager.TrafficViewEnrollmentStatusDisabled), + }, + }} + resp.SetResponse(http.StatusOK, profileResp, nil) + default: + errResp.SetResponseError(http.StatusBadRequest, "BadRequestError") + } + return resp, errResp +} + +// Delete returns the http status code based on the profileName. +func Delete(_ context.Context, resourceGroupName string, profileName string, _ *armtrafficmanager.ProfilesClientDeleteOptions) (resp azcorefake.Responder[armtrafficmanager.ProfilesClientDeleteResponse], errResp azcorefake.ErrorResponder) { + if resourceGroupName != DefaultResourceGroupName { + errResp.SetResponseError(http.StatusNotFound, "ResourceGroupNotFound") + return resp, errResp + } + switch profileName { + case ValidProfileName: + profileResp := armtrafficmanager.ProfilesClientDeleteResponse{} + resp.SetResponse(http.StatusOK, profileResp, nil) + default: + errResp.SetResponseError(http.StatusNotFound, "NotFound") + } + return resp, errResp +} diff --git a/test/common/trafficmanager/validator/profile.go b/test/common/trafficmanager/validator/profile.go new file mode 100644 index 00000000..84a06603 --- /dev/null +++ b/test/common/trafficmanager/validator/profile.go @@ -0,0 +1,68 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + +package validator + +import ( + "context" + "fmt" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + fleetnetv1alpha1 "go.goms.io/fleet-networking/api/v1alpha1" +) + +const ( + timeout = time.Second * 10 + interval = time.Millisecond * 250 +) + +var ( + commonCmpOptions = cmp.Options{ + cmpopts.IgnoreFields(metav1.ObjectMeta{}, "ResourceVersion", "UID", "CreationTimestamp", "ManagedFields", "Generation"), + cmpopts.IgnoreFields(metav1.OwnerReference{}, "UID"), + } + cmpTrafficManagerProfileOptions = cmp.Options{ + commonCmpOptions, + cmpopts.IgnoreFields(fleetnetv1alpha1.TrafficManagerProfile{}, "TypeMeta"), + cmpopts.IgnoreFields(metav1.Condition{}, "Message", "LastTransitionTime", "ObservedGeneration"), + cmpopts.SortSlices(func(c1, c2 metav1.Condition) bool { + return c1.Type < c2.Type + }), + } +) + +// ValidateTrafficManagerProfile validates the trafficManagerProfile object. +func ValidateTrafficManagerProfile(ctx context.Context, k8sClient client.Client, want *fleetnetv1alpha1.TrafficManagerProfile) { + key := types.NamespacedName{Name: want.Name, Namespace: want.Namespace} + profile := &fleetnetv1alpha1.TrafficManagerProfile{} + gomega.Eventually(func() error { + if err := k8sClient.Get(ctx, key, profile); err != nil { + return err + } + if diff := cmp.Diff(want, profile, cmpTrafficManagerProfileOptions); diff != "" { + return fmt.Errorf("trafficManagerProfile mismatch (-want, +got) :\n%s", diff) + } + return nil + }, timeout, interval).Should(gomega.Succeed(), "Get() trafficManagerProfile mismatch") +} + +// IsTrafficManagerProfileDeleted validates whether the profile is deleted or not. +func IsTrafficManagerProfileDeleted(ctx context.Context, k8sClient client.Client, name types.NamespacedName) { + gomega.Eventually(func() error { + profile := &fleetnetv1alpha1.TrafficManagerProfile{} + if err := k8sClient.Get(ctx, name, profile); !errors.IsNotFound(err) { + return fmt.Errorf("trafficManagerProfile %s still exists or an unexpected error occurred: %w", name, err) + } + return nil + }, timeout, interval).Should(gomega.Succeed(), "Failed to remove trafficManagerProfile %s ", name) +} diff --git a/test/scripts/bootstrap.sh b/test/scripts/bootstrap.sh index 7553b290..5ddbaa10 100644 --- a/test/scripts/bootstrap.sh +++ b/test/scripts/bootstrap.sh @@ -168,7 +168,7 @@ fi kubectl config use-context $HUB_CLUSTER-admin # need to make sure the version matches the one in the go.mod # workaround mentioned in https://github.com/kubernetes-sigs/controller-runtime/issues/1191 -kubectl apply -f `go env GOPATH`/pkg/mod/go.goms.io/fleet@v0.10.5/config/crd/bases/cluster.kubernetes-fleet.io_internalmemberclusters.yaml +kubectl apply -f `go env GOPATH`/pkg/mod/go.goms.io/fleet@v0.10.10/config/crd/bases/cluster.kubernetes-fleet.io_internalmemberclusters.yaml kubectl apply -f config/crd/* helm install hub-net-controller-manager \ ./charts/hub-net-controller-manager/ \