1
0
mirror of https://github.com/containous/traefik.git synced 2025-09-26 01:44:23 +03:00

Compare commits

...

44 Commits

Author SHA1 Message Date
mpl
74ad83f05a Prepare release v1.7.18 2019-09-26 15:46:05 +02:00
Ludovic Fernandez
d707c8ba93 Prepare release v1.7.17 2019-09-23 19:48:04 +02:00
Nicholas Wiersma
640eb62ca1 Avoid closing stdout when the accesslog handler is closed
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
Co-authored-by: jlevesy <julien.levesy@containo.us>
2019-09-23 14:50:06 +02:00
mpl
216710864e Actually send header and code during WriteHeader, if needed
Co-authored-by: Julien Salleyron <julien.salleyron@gmail.com>
2019-09-20 18:42:03 +02:00
Brad Jones
226f20b626 Add note clarifying client certificate header 2019-09-19 09:06:03 +02:00
Ludovic Fernandez
151be83bce Update docs links. 2019-09-18 15:38:04 +02:00
Ludovic Fernandez
d1a8c7fa78 Update Traefik image version. 2019-09-17 14:12:04 +02:00
Ludovic Fernandez
254dc38c3d Prepare release v1.7.16 2019-09-13 15:04:04 +02:00
mpl
24d084d7e6 implement Flusher and Hijacker for codeCatcher 2019-09-13 14:32:03 +02:00
Ludovic Fernandez
df5f530058 Prepare release v1.7.15 2019-09-12 18:10:05 +02:00
mpl
753d173965 error pages: do not buffer response when it's not an error 2019-09-12 16:20:05 +02:00
Daniel Tomcej
ffd1f122de Add TLS minversion constraint 2019-09-12 14:48:05 +02:00
Piotr Majkrzak
f98b57fdf4 Fix wrong handling of insecure tls auth forward ingress annotation 2019-09-12 11:44:05 +02:00
Damien Duportal
2d37f08864 Improve Access Logs Documentation page 2019-09-11 18:14:03 +02:00
Nicholas Wiersma
a7dbcc282c Consider default cert domain in certificate store
Co-authored-by: Nicolas Mengin <nmengin.pro@gmail.com>
2019-09-11 17:46:04 +02:00
David Dymko
f4f62e7fb3 Update Acme doc - Vultr Wildcard & Root 2019-09-09 09:26:04 +02:00
mpl
4cae8bcb10 Finish kubernetes throttling refactoring 2019-08-31 05:10:04 -07:00
Ben Weissmann
bee370ec6b Throttle Kubernetes config refresh 2019-08-30 03:16:04 -07:00
pitan
f1d016b893 Typo in basic auth usersFile label consul-catalog 2019-08-21 01:36:03 -07:00
Erik Wegner
4defbbe848 Kubernetes support for Auth.HeaderField 2019-08-21 01:30:05 -07:00
Ludovic Fernandez
f397342f16 Prepare release v1.7.14 2019-08-14 02:06:03 -07:00
Ludovic Fernandez
989a59cc29 Update to go1.12.8 2019-08-14 01:52:05 -07:00
Julien Levesy
c5b71592c8 Make hijackConnectionTracker.Close thread safe 2019-08-12 02:34:04 -07:00
Ludovic Fernandez
9de3129a55 Prepare release v1.7.13 2019-08-08 09:04:03 -07:00
Douglas Wagner
40ab1f325c Wrr loadbalancer honors old weight on recovered servers 2019-08-07 08:14:04 -07:00
Daniel Becker
73e0561610 Update lego 2019-08-05 09:08:04 -07:00
Jean Prat
0a89cccdc0 warning should not be a fail status 2019-08-05 08:40:04 -07:00
Daniel Tomcej
b102e6de5a Add missing KeyUsages for default generated certificate 2019-08-05 06:20:05 -07:00
alvarezbruned
2064a6f805 Add example for CLI 2019-08-01 05:38:04 -07:00
Amir Keibi
72e2ddff98 Updating Service Fabric documentation 2019-07-26 02:30:05 -07:00
David Dymko
7db41967c6 Fixed doc link for AlibabaCloud 2019-07-16 16:02:04 +02:00
Jean-Baptiste Doumenjou
a8680a8719 Fixes the TLS Mutual Authentication documentation 2019-07-15 08:46:04 +02:00
Ludovic Fernandez
7e11fa1193 Update Docker version. 2019-07-15 07:04:02 +02:00
gurayyildirim
489b5a6150 Format YAML example on user guide 2019-07-08 10:40:03 +02:00
Daniel Tomcej
8027e8ee23 Check for multiport services on Global Backend Ingress 2019-06-27 09:42:03 +02:00
Damien Duportal
d35d5bd2e8 Update documentation to clarify the default format for logs 2019-06-26 14:28:04 +02:00
stffabi
8e47bdedc6 Clear TLS client headers if TLSMutualAuth is optional 2019-06-26 11:32:04 +02:00
Damien Duportal
51419a9235 Update Slack support channel references to Discourse community forum 2019-06-25 10:06:03 +02:00
Ludovic Fernandez
32fd52c698 Use dynamodbav tags to override json tags. 2019-06-24 09:14:03 +02:00
Ludovic Fernandez
468e4ebd98 Add remarks about Rancher 2 2019-06-21 19:04:04 +02:00
Damien Duportal
92a57384a4 Allows logs to use local time zone instead of UTC
Co-authored-by: Ludovic Fernandez <ldez@users.noreply.github.com>
2019-06-17 09:20:03 +02:00
Damien Duportal
2067a433cd Improve API / Dashboard wording in documentation 2019-06-04 00:42:05 +02:00
kolaente
16f1f851cc Use the latest stable version of traefik in the docs 2019-06-03 17:38:07 +02:00
Daniel Tomcej
8e992c7cfb Update docker api version 2019-06-03 11:28:06 +02:00
851 changed files with 115002 additions and 17771 deletions

20
.semaphoreci/golang.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -e
curl -O https://dl.google.com/go/go1.12.linux-amd64.tar.gz
tar -xvf go1.12.linux-amd64.tar.gz
rm -rf go1.12.linux-amd64.tar.gz
sudo mkdir -p /usr/local/golang/1.12/go
sudo mv go /usr/local/golang/1.12/
sudo rm /usr/local/bin/go
sudo chmod +x /usr/local/golang/1.12/go/bin/go
sudo ln -s /usr/local/golang/1.12/go/bin/go /usr/local/bin/go
export GOROOT="/usr/local/golang/1.12/go"
export GOTOOLDIR="/usr/local/golang/1.12/go/pkg/tool/linux_amd64"
go version

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -e
export DOCKER_VERSION=17.03.1
export DOCKER_VERSION=18.09.7
source .semaphoreci/vars

View File

@@ -13,6 +13,7 @@ env:
- VERSION: $TRAVIS_TAG
- CODENAME: maroilles
- N_MAKE_JOBS: 2
- DOCS_VERIFY_SKIP: true
script:
- echo "Skipping tests... (Tests are executed on SemaphoreCI)"

View File

@@ -1,5 +1,79 @@
# Change Log
## [v1.7.18](https://github.com/containous/traefik/tree/v1.7.18) (2019-09-23)
[All Commits](https://github.com/containous/traefik/compare/v1.7.17...v1.7.18)
**Bug fixes:**
- **[go,security]** This version is compiled with [Go 1.12.10](https://groups.google.com/d/msg/golang-announce/cszieYyuL9Q/g4Z7pKaqAgAJ), which fixes a vulnerability in previous versions. See the [CVE](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-16276) about it for more details.
## [v1.7.17](https://github.com/containous/traefik/tree/v1.7.17) (2019-09-23)
[All Commits](https://github.com/containous/traefik/compare/v1.7.16...v1.7.17)
**Bug fixes:**
- **[logs,middleware]** Avoid closing stdout when the accesslog handler is closed ([#5459](https://github.com/containous/traefik/pull/5459) by [nrwiersma](https://github.com/nrwiersma))
- **[middleware]** Actually send header and code during WriteHeader, if needed ([#5404](https://github.com/containous/traefik/pull/5404) by [mpl](https://github.com/mpl))
**Documentation:**
- **[k8s]** Add note clarifying client certificate header ([#5362](https://github.com/containous/traefik/pull/5362) by [bradjones1](https://github.com/bradjones1))
- **[webui]** Update docs links. ([#5412](https://github.com/containous/traefik/pull/5412) by [ldez](https://github.com/ldez))
- Update Traefik image version. ([#5399](https://github.com/containous/traefik/pull/5399) by [ldez](https://github.com/ldez))
## [v1.7.16](https://github.com/containous/traefik/tree/v1.7.16) (2019-09-13)
[All Commits](https://github.com/containous/traefik/compare/v1.7.15...v1.7.16)
**Bug fixes:**
- **[middleware,websocket]** implement Flusher and Hijacker for codeCatcher ([#5376](https://github.com/containous/traefik/pull/5376) by [mpl](https://github.com/mpl))
## [v1.7.15](https://github.com/containous/traefik/tree/v1.7.15) (2019-09-12)
[All Commits](https://github.com/containous/traefik/compare/v1.7.14...v1.7.15)
**Bug fixes:**
- **[authentication,k8s/ingress]** Kubernetes support for Auth.HeaderField ([#5235](https://github.com/containous/traefik/pull/5235) by [ErikWegner](https://github.com/ErikWegner))
- **[k8s,k8s/ingress]** Finish kubernetes throttling refactoring ([#5269](https://github.com/containous/traefik/pull/5269) by [mpl](https://github.com/mpl))
- **[k8s]** Throttle Kubernetes config refresh ([#4716](https://github.com/containous/traefik/pull/4716) by [benweissmann](https://github.com/benweissmann))
- **[k8s]** Fix wrong handling of insecure tls auth forward ingress annotation ([#5319](https://github.com/containous/traefik/pull/5319) by [majkrzak](https://github.com/majkrzak))
- **[middleware]** error pages: do not buffer response when it&#39;s not an error ([#5285](https://github.com/containous/traefik/pull/5285) by [mpl](https://github.com/mpl))
- **[tls]** Consider default cert domain in certificate store ([#5353](https://github.com/containous/traefik/pull/5353) by [nrwiersma](https://github.com/nrwiersma))
- **[tls]** Add TLS minversion constraint ([#5356](https://github.com/containous/traefik/pull/5356) by [dtomcej](https://github.com/dtomcej))
**Documentation:**
- **[acme]** Update Acme doc - Vultr Wildcard &amp; Root ([#5320](https://github.com/containous/traefik/pull/5320) by [ddymko](https://github.com/ddymko))
- **[consulcatalog]** Typo in basic auth usersFile label consul-catalog ([#5230](https://github.com/containous/traefik/pull/5230) by [pitan](https://github.com/pitan))
- **[logs]** Improve Access Logs Documentation page ([#5238](https://github.com/containous/traefik/pull/5238) by [dduportal](https://github.com/dduportal))
## [v1.7.14](https://github.com/containous/traefik/tree/v1.7.14) (2019-08-14)
[All Commits](https://github.com/containous/traefik/compare/v1.7.13...v1.7.14)
**Bug fixes:**
- Update to go1.12.8 ([#5201](https://github.com/containous/traefik/pull/5201) by [ldez](https://github.com/ldez)). HTTP/2 Denial of Service [CVE-2019-9512](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9512) and [CVE-2019-9514](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-9514)
- **[server]** Make hijackConnectionTracker.Close thread safe ([#5194](https://github.com/containous/traefik/pull/5194) by [jlevesy](https://github.com/jlevesy))
## [v1.7.13](https://github.com/containous/traefik/tree/v1.7.13) (2019-08-07)
[All Commits](https://github.com/containous/traefik/compare/v1.7.12...v1.7.13)
**Bug fixes:**
- **[acme]** Update lego ([#5166](https://github.com/containous/traefik/pull/5166) by [dabeck](https://github.com/dabeck))
- **[consulcatalog]** warning should not be a fail status ([#4537](https://github.com/containous/traefik/pull/4537) by [saez0pub](https://github.com/saez0pub))
- **[docker]** Update docker api version ([#4909](https://github.com/containous/traefik/pull/4909) by [dtomcej](https://github.com/dtomcej))
- **[dynamodb]** Use dynamodbav tags to override json tags. ([#5002](https://github.com/containous/traefik/pull/5002) by [ldez](https://github.com/ldez))
- **[healthcheck]** Wrr loadbalancer honors old weight on recovered servers ([#5051](https://github.com/containous/traefik/pull/5051) by [DougWagner](https://github.com/DougWagner))
- **[k8s]** Check for multiport services on Global Backend Ingress ([#5021](https://github.com/containous/traefik/pull/5021) by [dtomcej](https://github.com/dtomcej))
- **[logs]** Allows logs to use local time zone instead of UTC ([#4954](https://github.com/containous/traefik/pull/4954) by [dduportal](https://github.com/dduportal))
- **[middleware]** Clear TLS client headers if TLSMutualAuth is optional ([#4963](https://github.com/containous/traefik/pull/4963) by [stffabi](https://github.com/stffabi))
- **[tls]** Add missing KeyUsages for default generated certificate ([#5150](https://github.com/containous/traefik/pull/5150) by [dtomcej](https://github.com/dtomcej))
**Documentation:**
- **[acme]** Fixed doc link for AlibabaCloud ([#5109](https://github.com/containous/traefik/pull/5109) by [ddymko](https://github.com/ddymko))
- **[docker]** Add example for CLI ([#5131](https://github.com/containous/traefik/pull/5131) by [alvarezbruned](https://github.com/alvarezbruned))
- **[docker]** Use the latest stable version of traefik in the docs ([#4927](https://github.com/containous/traefik/pull/4927) by [kolaente](https://github.com/kolaente))
- **[logs]** Update documentation to clarify the default format for logs ([#4953](https://github.com/containous/traefik/pull/4953) by [dduportal](https://github.com/dduportal))
- **[rancher]** Add remarks about Rancher 2 ([#4999](https://github.com/containous/traefik/pull/4999) by [ldez](https://github.com/ldez))
- **[tls]** Fixes the TLS Mutual Authentication documentation ([#5085](https://github.com/containous/traefik/pull/5085) by [jbdoumenjou](https://github.com/jbdoumenjou))
- Format YAML example on user guide ([#5067](https://github.com/containous/traefik/pull/5067) by [gurayyildirim](https://github.com/gurayyildirim))
- Update Slack support channel references to Discourse community forum ([#5014](https://github.com/containous/traefik/pull/5014) by [dduportal](https://github.com/dduportal))
- Updating Service Fabric documentation ([#5160](https://github.com/containous/traefik/pull/5160) by [gheibia](https://github.com/gheibia))
- Improve API / Dashboard wording in documentation ([#4929](https://github.com/containous/traefik/pull/4929) by [dduportal](https://github.com/dduportal))
## [v1.7.12](https://github.com/containous/traefik/tree/v1.7.12) (2019-05-29)
[All Commits](https://github.com/containous/traefik/compare/v1.7.11...v1.7.12)

View File

@@ -13,7 +13,7 @@ You need to run the `binary` target. This will create binaries for Linux platfor
$ make binary
docker build -t "traefik-dev:no-more-godep-ever" -f build.Dockerfile .
Sending build context to Docker daemon 295.3 MB
Step 0 : FROM golang:1.11-alpine
Step 0 : FROM golang:1.12-alpine
---> 8c6473912976
Step 1 : RUN go get github.com/golang/dep/cmd/dep
[...]
@@ -158,7 +158,7 @@ Integration tests must be run from the `integration/` directory and require the
## Documentation
The [documentation site](https://docs.traefik.io/) is built with [mkdocs](https://mkdocs.org/)
The [documentation site](https://docs.traefik.io/v1.7/) is built with [mkdocs](https://mkdocs.org/)
### Building Documentation

267
Gopkg.lock generated
View File

@@ -17,6 +17,14 @@
pruneopts = "NUT"
revision = "02e53af36e6c978af692887ed449b74026d76fec"
[[projects]]
digest = "1:3fb854fc35fafced21615a93db3c0e324c8cb1d5a89f39dd1e72db0c4200345a"
name = "contrib.go.opencensus.io/exporter/ocagent"
packages = ["."]
pruneopts = "NUT"
revision = "dcb33c7f3b7cfe67e8a2cea10207ede1b7c40764"
version = "v0.4.12"
[[projects]]
branch = "master"
digest = "1:24afd6a7be85997c981693ddcc5f7b37fd3ed5cb119901e14737b9635b944cfb"
@@ -34,15 +42,15 @@
revision = "72eebf980f467d3ab3a8b4ddf660f664911ce519"
[[projects]]
digest = "1:59f6c2fd10ad014d2907eaa48a19070d5e7af35624328a4fbbf93b7b1c4a19e8"
digest = "1:1a06fd7e9325a0b2d1b44ff0253afac49857863cb86ba30ea8212690b7c6287e"
name = "github.com/Azure/azure-sdk-for-go"
packages = [
"services/dns/mgmt/2017-09-01/dns",
"version",
]
pruneopts = "NUT"
revision = "068ec4d616be5b2175509bf1fb3e4c8ea160d5c8"
version = "v15.0.1"
revision = "98f2f9ff6ad63a307bb3438b7631af594b11f32b"
version = "v31.2.0"
[[projects]]
branch = "master"
@@ -56,19 +64,23 @@
revision = "d6e3b3328b783f23731bc4d058875b0371ff8109"
[[projects]]
digest = "1:2526235ff8cae0162b34e8af3779621de71b3a020dead9bcce74d3f43a18c988"
digest = "1:847ae8f26c6796f0dbed826b7bc0297eae976fa2b24045e3e2ddb26846378a65"
name = "github.com/Azure/go-autorest"
packages = [
"autorest",
"autorest/adal",
"autorest/azure",
"autorest/azure/auth",
"autorest/azure/cli",
"autorest/date",
"autorest/to",
"autorest/validation",
"logger",
"tracing",
]
pruneopts = "NUT"
revision = "9ad9326b278af8fa5cc67c30c0ce9a58cc0862b2"
version = "v10.6.0"
revision = "ba1147dc57f993013ef255c128ca1cac8a557409"
version = "v12.4.1"
[[projects]]
branch = "master"
@@ -89,13 +101,6 @@
pruneopts = "NUT"
revision = "6add9cd6ad42d389d6ead1dde60b4ad71e46fd74"
[[projects]]
digest = "1:ab7fee312bbdc8070d0325d841de8704cc78bf032b076200f1458659b74b8ed6"
name = "github.com/JamesClonk/vultr"
packages = ["lib"]
pruneopts = "NUT"
revision = "2fd0705ce648e602e6c9c57329a174270a4f6688"
[[projects]]
digest = "1:25870183293a3fb61cc9afd060a61d63a486f091db72af01a8ea3449f5ca530d"
name = "github.com/Masterminds/goutils"
@@ -215,7 +220,7 @@
revision = "063d875e3c5fd734fa2aa12fac83829f62acfc70"
[[projects]]
digest = "1:47071ecf8d840dd357ede1b2aed46576bdd0a866adecef3c9e85a00db9672202"
digest = "1:dddee1f9ce7caecc95ae089c721a65ed42b35f8cc8a8b8a3ee6e3758ec93ec4b"
name = "github.com/akamai/AkamaiOPEN-edgegrid-golang"
packages = [
"client-v1",
@@ -224,16 +229,17 @@
"jsonhooks-v1",
]
pruneopts = "NUT"
revision = "1471ce9c14c6d8c007516e129262962a628fecdf"
version = "v0.7.3"
revision = "009960c8b2c7c57a0c5c488a3c8c778c16f3f586"
version = "v0.7.4"
[[projects]]
digest = "1:823e87ae25170339e2bfd1d6f7c2e27554c6bb5655f91c67b37bd5be45bb6b32"
digest = "1:cd8348ab7b1ce85f965157e966791bb13514ea933902ecd1c010b5c529657f3d"
name = "github.com/aliyun/alibaba-cloud-sdk-go"
packages = [
"sdk",
"sdk/auth",
"sdk/auth/credentials",
"sdk/auth/credentials/provider",
"sdk/auth/signers",
"sdk/endpoints",
"sdk/errors",
@@ -243,8 +249,8 @@
"services/alidns",
]
pruneopts = "NUT"
revision = "cad214d7d71fba7883fcf3b7e550ba782c15b400"
version = "1.27.7"
revision = "26f0a5c2d5dc6d4a73c62651d146bdd51dc3bdde"
version = "1.60.84"
[[projects]]
digest = "1:b39cf81d5f440b9c0757a25058432d33af867e5201109bf53621356d9dab4b73"
@@ -263,7 +269,7 @@
revision = "48572f11356f1843b694f21a290d4f1006bc5e47"
[[projects]]
digest = "1:bfb036834a43e76abd318f0db39b0bbec6f7865680c1e443475c0297250a89ed"
digest = "1:bd56ffde8e12e9faca92f3ac54347c665d931856fba8784007b02e004fbcc30d"
name = "github.com/aws/aws-sdk-go"
packages = [
"aws",
@@ -275,15 +281,20 @@
"aws/credentials",
"aws/credentials/ec2rolecreds",
"aws/credentials/endpointcreds",
"aws/credentials/processcreds",
"aws/credentials/stscreds",
"aws/crr",
"aws/csm",
"aws/defaults",
"aws/ec2metadata",
"aws/endpoints",
"aws/request",
"aws/session",
"aws/signer/v4",
"internal/ini",
"internal/sdkio",
"internal/sdkrand",
"internal/sdkuri",
"internal/shareddefaults",
"private/protocol",
"private/protocol/ec2query",
@@ -302,10 +313,11 @@
"service/lightsail",
"service/route53",
"service/sts",
"service/sts/stsiface",
]
pruneopts = "NUT"
revision = "fad131ddc707880428615dc8bc1587b55fb46d74"
version = "v1.13.54"
revision = "f3fe96892dc0e8e8e9862456e1455e62e4cb55d4"
version = "v1.21.9"
[[projects]]
branch = "master"
@@ -332,12 +344,27 @@
version = "v2.1.1"
[[projects]]
digest = "1:03cfacdc6bfd46007c15786c1ece3fa074f89e5193a292f0f26d9e98c99c7cc2"
digest = "1:8f5acd4d4462b5136af644d25101f0968a7a94ee90fcb2059cec5b7cc42e0b20"
name = "github.com/census-instrumentation/opencensus-proto"
packages = [
"gen-go/agent/common/v1",
"gen-go/agent/metrics/v1",
"gen-go/agent/trace/v1",
"gen-go/metrics/v1",
"gen-go/resource/v1",
"gen-go/trace/v1",
]
pruneopts = "NUT"
revision = "d89fa54de508111353cb0b06403c00569be780d8"
version = "v0.2.1"
[[projects]]
digest = "1:b63319bd1e224704c8a4d7d65c82791ef5ceb168b7c2c028193595064c910f1a"
name = "github.com/cloudflare/cloudflare-go"
packages = ["."]
pruneopts = "NUT"
revision = "1f9007fbecae20711133c60519338c41cef1ffb4"
version = "v0.8.5"
revision = "b59d4f05eec361422b388c7ed74293ce8a3d5f97"
version = "v0.9.4"
[[projects]]
branch = "master"
@@ -450,12 +477,12 @@
version = "v1.1.0"
[[projects]]
branch = "master"
digest = "1:d3972abc351cd5578e3281d332ac9e7a04dafc6e5af97c064fe396c4f90ee769"
digest = "1:fa62421bd924623ac10a160686cc55d529f7274b2caedf7d2c607d14bc50c118"
name = "github.com/decker502/dnspod-go"
packages = ["."]
pruneopts = "NUT"
revision = "f33a2c6040fc2550a631de7b3a53bddccdcd73fb"
revision = "71fbbdbdf1a7eeac949586de15bf96d416d3dd63"
version = "v0.2.0"
[[projects]]
digest = "1:7a6852b35eb5bbc184561443762d225116ae630c26a7c4d90546619f1e7d2ad2"
@@ -466,20 +493,20 @@
version = "v3.2.0"
[[projects]]
branch = "master"
digest = "1:f9adc21a937e5da643ea14a3488cb7506788876737a5e205394e508627a6eec8"
name = "github.com/dimchansky/utfbom"
packages = ["."]
pruneopts = "NUT"
revision = "d2133a1ce379ef6fa992b0514a77146c60db9d1c"
version = "v1.1.0"
[[projects]]
digest = "1:e856fc44ab196970612bdc8c15e65ccf92ed8d4ccb3a2e65b88dc240a2fe5d0b"
digest = "1:d9688055094edd046a71ee85204ea97090ecce6f316b739be3a50f5ca6403ebb"
name = "github.com/dnsimple/dnsimple-go"
packages = ["dnsimple"]
pruneopts = "NUT"
revision = "f5ead9c20763fd925dea1362f2af5d671ed2a459"
version = "v0.21.0"
revision = "7e193cc468a07cdf74d76de1e6736c709c5a3872"
version = "v0.30.0"
[[projects]]
digest = "1:cf7cba074c4d2f8e2a5cc2f10b1f6762c86cff2e39917b9f9a6dbd7df57fe9c9"
@@ -690,12 +717,12 @@
revision = "89ef8af493ab468a45a42bb0d89a06fccdd2fb22"
[[projects]]
digest = "1:6f26e34204ddad638d25456c6443c2763aa38954558226722c253e08e9f491fa"
digest = "1:efcc176c5cfbe8421dd3eb2562be91c3272e80f61b181e31d5745b7fee29a669"
name = "github.com/exoscale/egoscale"
packages = ["."]
pruneopts = "NUT"
revision = "0a91ac8209d6a805f259ff881d0c2654221d0346"
version = "v0.14.3"
revision = "8f608c40ae891e0240bb6e696a72437be7069d83"
version = "v0.18.1"
[[projects]]
digest = "1:b0d5e98ac0f0a509eb320f542e748582d637aae09e74538212e9712d1e71064b"
@@ -735,7 +762,7 @@
revision = "73d445a93680fa1a78ae23a5839bad48f32ba1ee"
[[projects]]
digest = "1:d82b2dc81c551e7c15f31523a2cc8ee9121b39cfbf63174d98a0bc8edf2d3c5e"
digest = "1:896402a6d43c4fb1e21512e7473a51e7fd2b03a4e1a420a730a4b712d20e5d6a"
name = "github.com/go-acme/lego"
packages = [
"acme",
@@ -798,6 +825,7 @@
"providers/dns/mydnsjp",
"providers/dns/namecheap",
"providers/dns/namedotcom",
"providers/dns/namesilo",
"providers/dns/netcup",
"providers/dns/netcup/internal",
"providers/dns/nifcloud",
@@ -816,6 +844,7 @@
"providers/dns/stackpath",
"providers/dns/transip",
"providers/dns/vegadns",
"providers/dns/versio",
"providers/dns/vscale",
"providers/dns/vscale/internal",
"providers/dns/vultr",
@@ -823,8 +852,8 @@
"registration",
]
pruneopts = "NUT"
revision = "01903cdfb9869df45cf5274c53226823a2532f2d"
version = "v2.6.0"
revision = "295dd66f2aa549685e3d6b95c1813881a7807158"
version = "v2.7.2"
[[projects]]
branch = "fork-containous"
@@ -906,14 +935,6 @@
pruneopts = "NUT"
revision = "1d0bd113de87027671077d3c71eb3ac5d7dbba72"
[[projects]]
digest = "1:00f1b1d722a012f54670ecc09ee92557d7314bfd9152a085ce91933c1c14b026"
name = "github.com/go-resty/resty"
packages = ["."]
pruneopts = "NUT"
revision = "d4920dcf5b7689548a6db640278a9b35a5b48ec6"
version = "v1.9.1"
[[projects]]
digest = "1:8cf58169eb0a8c009ed3a4c36486980d602ab4cc4e478130493d6cd0404f889b"
name = "github.com/go-stack/stack"
@@ -922,6 +943,14 @@
revision = "54be5f394ed2c3e19dac9134a40a95ba5a017f7b"
version = "v1.5.4"
[[projects]]
digest = "1:bde9f189072512ba353f3641d4839cb4c9c7edf421e467f2c03f267b402bd16c"
name = "github.com/gofrs/uuid"
packages = ["."]
pruneopts = "NUT"
revision = "6b08a5c5172ba18946672b49749cde22873dd7c2"
version = "v3.2.0"
[[projects]]
digest = "1:b518b9be1fc76244e246afe09113e3dd6246073b444787d30883877b82a0b90d"
name = "github.com/gogo/protobuf"
@@ -943,18 +972,25 @@
revision = "44145f04b68cf362d9c4df2182967c2275eaefed"
[[projects]]
digest = "1:03e14cff610a8a58b774e36bd337fa979482be86aab01be81fb8bbd6d0f07fc8"
digest = "1:1e21a0bbb10d73b781e87a9a123b96f6ebe08d0451e50f1b97c1213b302af46f"
name = "github.com/golang/protobuf"
packages = [
"jsonpb",
"proto",
"protoc-gen-go/descriptor",
"protoc-gen-go/generator",
"protoc-gen-go/generator/internal/remap",
"protoc-gen-go/plugin",
"ptypes",
"ptypes/any",
"ptypes/duration",
"ptypes/struct",
"ptypes/timestamp",
"ptypes/wrappers",
]
pruneopts = "NUT"
revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265"
version = "v1.1.0"
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
version = "v1.3.2"
[[projects]]
branch = "master"
@@ -1073,6 +1109,18 @@
pruneopts = "NUT"
revision = "2bcd89a1743fd4b373f7370ce8ddc14dfbd18229"
[[projects]]
digest = "1:3107a6ef51bb9f3bc7cf235af0d92303b39afaa841f02dfc87f1d0a5d05a3fca"
name = "github.com/grpc-ecosystem/grpc-gateway"
packages = [
"internal",
"runtime",
"utilities",
]
pruneopts = "NUT"
revision = "ad529a448ba494a88058f9e5be0988713174ac86"
version = "v1.9.5"
[[projects]]
digest = "1:c3f14b698c0f5c5729896489f4b526f519d1d2522e697d63f532901d0e183dff"
name = "github.com/hashicorp/consul"
@@ -1176,11 +1224,11 @@
revision = "8eebe170fa1ba25d3dfb928b3f86a7313b13b9fe"
[[projects]]
digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc"
digest = "1:1f2aebae7e7c856562355ec0198d8ca2fa222fb05e5b1b66632a1fce39631885"
name = "github.com/jmespath/go-jmespath"
packages = ["."]
pruneopts = "NUT"
revision = "0b12d6b5"
revision = "c2b33e84"
[[projects]]
digest = "1:273d270076cfc02c2d481b187203ebb3891825f46fe78b925141a22631c52503"
@@ -1190,12 +1238,12 @@
revision = "72f9bd7c4e0c2a40055ab3d0f09654f730cce982"
[[projects]]
digest = "1:63d8b364f0768ffda64b8e6e15c10535f3431e3c69d051dbb37f467ada02df98"
digest = "1:92f6419f388bd31a433b1388910f15a882c9980e383e89ebf8fb2524583707ac"
name = "github.com/json-iterator/go"
packages = ["."]
pruneopts = "NUT"
revision = "28452fcdec4e44348d2af0d91d1e9e38da3a9e0a"
version = "1.0.5"
revision = "27518f6661eba504be5a7a9a9f6d9460d892ade3"
version = "v1.1.7"
[[projects]]
digest = "1:8b3234b10eacd5edea45bf0c13a585b608749da23f94aaf29b46d9ef8a8babf4"
@@ -1276,12 +1324,12 @@
revision = "1113af38e5916529ad7317b0fe12e273e6e92af5"
[[projects]]
digest = "1:111ff5a09a32895248270bfaef9b8b6ac163a8cde9cdd603fed64b3e4b59e8ab"
digest = "1:7981caf0d67995c0acbd352785f45b177a2d089ed40f2005794c0772d0fbfff9"
name = "github.com/linode/linodego"
packages = ["."]
pruneopts = "NUT"
revision = "d0d31d8ca62fa3f7e4526ca0ce95de81e4ed001e"
version = "v0.5.1"
revision = "7adba57685c129bcd29a9edc7008ec3b05680240"
version = "v0.10.0"
[[projects]]
digest = "1:196b0d7580e898df15a7cc5371cbfe2b8e22904f5c6c883ed5db0130e551c8fb"
@@ -1394,12 +1442,12 @@
revision = "d23ffcb85de31694d6ccaa23ccb4a03e55c1303f"
[[projects]]
branch = "master"
digest = "1:b62c4f18ad6eb454ac5253e7791ded3d7867330015ca4b37b6336e57f514585e"
digest = "1:f9f72e583aaacf1d1ac5d6121abd4afd3c690baa9e14e1d009df26bf831ba347"
name = "github.com/mitchellh/go-homedir"
packages = ["."]
pruneopts = "NUT"
revision = "b8bc1bf767474819792c23f32d8286a45736f1c6"
revision = "af06845cf3004701891bf4fdb884bfe4920b3727"
version = "v1.1.0"
[[projects]]
branch = "master"
@@ -1425,6 +1473,22 @@
pruneopts = "NUT"
revision = "63d60e9d0dbc60cf9164e6510889b0db6683d98c"
[[projects]]
digest = "1:2f42fa12d6911c7b7659738758631bec870b7e9b4c6be5444f963cdcfccc191f"
name = "github.com/modern-go/concurrent"
packages = ["."]
pruneopts = "NUT"
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
version = "1.0.3"
[[projects]]
digest = "1:c6aca19413b13dc59c220ad7430329e2ec454cc310bc6d8de2c7e2b93c18a0f6"
name = "github.com/modern-go/reflect2"
packages = ["."]
pruneopts = "NUT"
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
version = "1.0.1"
[[projects]]
digest = "1:7bb97a5f80a2429fa605e176e16cf682cbb5ac681f421a06efb4e3b8efae6e5f"
name = "github.com/mvdan/xurls"
@@ -1456,6 +1520,14 @@
revision = "d8152159450570012552f924a0ae6ab3d8c617e0"
version = "v0.6.0"
[[projects]]
digest = "1:a1e8eda841bca324a6b4d88824d625b3ba8d86bf9ec68677d595c02d7502918b"
name = "github.com/nrdcg/namesilo"
packages = ["."]
pruneopts = "NUT"
revision = "1b10d027904464789f889ee4cd76af15c3bf4943"
version = "v0.2.1"
[[projects]]
branch = "master"
digest = "1:95d27e49401b61dd203a4cf8237037bd6cd49599651f855ac1988c4ae27b090e"
@@ -1527,15 +1599,15 @@
version = "v0.3.5"
[[projects]]
digest = "1:8a5a270130e940b167027e8c9a07c20d01410bd6053160432351139ddc2cb501"
digest = "1:3b8fccf07d5825ca15ec7c14855ea3ff9cb62f565d8b0e1c97e0b37a0bee30b0"
name = "github.com/oracle/oci-go-sdk"
packages = [
"common",
"dns",
]
pruneopts = "NUT"
revision = "5295b14d42d1417490d318ff91475c5868e0fd8f"
version = "v4.1.0"
revision = "bf9cc5e4c798e1e0b0749ce89c861cb13f37862d"
version = "v5.15.0"
[[projects]]
branch = "master"
@@ -1678,7 +1750,7 @@
revision = "256dc444b735e061061cf46c809487313d5b0065"
[[projects]]
digest = "1:253f275bd72c42f8d234712d1574c8b222fe9b72838bfaca11b21ace9c0e3d0a"
digest = "1:09ba8f72048eacb4b5545d81392e594f4e5622cd670e517847c47b5a194bb31c"
name = "github.com/sacloud/libsacloud"
packages = [
".",
@@ -1688,8 +1760,8 @@
"utils/mutexkv",
]
pruneopts = "NUT"
revision = "41c392dee98a83260abbe0fcd5c13beb7c75d103"
version = "v1.21.1"
revision = "5294ac87ea76a7dffd4fb6b088f3a6ce6c4f91b9"
version = "v1.26.1"
[[projects]]
digest = "1:142520cf3c9bb85449dd0000f820b8c604531587ee654793c54909be7dabadac"
@@ -1698,14 +1770,6 @@
pruneopts = "NUT"
revision = "1d7be4effb13d2d908342d349d71a284a7542693"
[[projects]]
digest = "1:6bc0652ea6e39e22ccd522458b8bdd8665bf23bdc5a20eec90056e4dc7e273ca"
name = "github.com/satori/go.uuid"
packages = ["."]
pruneopts = "NUT"
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
digest = "1:01252cd79aac70f16cac02a72a1067dd136e0ad6d5b597d0129cf74c739fd8d1"
name = "github.com/sirupsen/logrus"
@@ -1776,8 +1840,7 @@
version = "1.0.2"
[[projects]]
branch = "master"
digest = "1:21ef4b12d6b6c6dfe389caa82f859d1e35eeb24ce61c60d09db57addb6a95781"
digest = "1:4da573eeacb95c9452ae7ddc14b06148a99b29bf98522abb221ef9799426187c"
name = "github.com/transip/gotransip"
packages = [
".",
@@ -1785,7 +1848,8 @@
"util",
]
pruneopts = "NUT"
revision = "c6e2ce0bbb4a601a909e3b7a773358d6b503e663"
revision = "efb64632cab7701ec33f1eaeaa738e2207efe68e"
version = "v5.14"
[[projects]]
digest = "1:9b2996458a2f7d1f3e0ebf08152acfe8c1106f3fe855d08121c5ee7d801a063f"
@@ -1891,6 +1955,14 @@
revision = "939c094524d124c55fa8afe0e077701db4a865e2"
version = "v1.0.0"
[[projects]]
digest = "1:b1d2c51a689eef501cb5726f8d9997c0ca4415cbfa7105fe6e64b1844eddf1fb"
name = "github.com/vultr/govultr"
packages = ["."]
pruneopts = "NUT"
revision = "ca447e056e08d93aa6e5b09e6ae3565dd1825281"
version = "v0.1.4"
[[projects]]
branch = "master"
digest = "1:669d39abbe79469bb50e32a26541026fd91326ac0ca56a9ad83be5b9887e5a8c"
@@ -1915,7 +1987,7 @@
revision = "0c8571ac0ce161a5feb57375a9cdf148c98c0f70"
[[projects]]
digest = "1:045bc0ab96bb83bdffd2606f019003da03d1c139d3cb8aad13596863e4dd37d6"
digest = "1:a6f0a3485a3edbe3d66a5036d5dd8be1da89b8e077d98bf8a95ccfdad854d6cf"
name = "go.opencensus.io"
packages = [
".",
@@ -1923,8 +1995,10 @@
"internal/tagencoding",
"metric/metricdata",
"metric/metricproducer",
"plugin/ocgrpc",
"plugin/ochttp",
"plugin/ochttp/propagation/b3",
"plugin/ochttp/propagation/tracecontext",
"resource",
"stats",
"stats/internal",
@@ -1936,8 +2010,8 @@
"trace/tracestate",
]
pruneopts = "NUT"
revision = "75c0cca22312e51bfd4fafdbe9197ae399e18b38"
version = "v0.20.2"
revision = "df6e2001952312404b06f5f6f03fcb4aec1648e5"
version = "v0.21.0"
[[projects]]
branch = "master"
@@ -2010,6 +2084,14 @@
pruneopts = "NUT"
revision = "ec22f46f877b4505e0117eeaab541714644fdd28"
[[projects]]
branch = "master"
digest = "1:382bb5a7fb4034db3b6a2d19e5a4a6bcf52f4750530603c01ca18a172fa3089b"
name = "golang.org/x/sync"
packages = ["semaphore"]
pruneopts = "NUT"
revision = "112230192c580c3556b8cee6403af37a4fc5f28c"
[[projects]]
branch = "master"
digest = "1:4e67fdd7a13cbdb3c0dff0a7505abbdf4f42b12b27da350d66bffdc700db2899"
@@ -2054,7 +2136,7 @@
[[projects]]
branch = "master"
digest = "1:70c173b8ecc111dd01dc07f0ada72c076e4ed91618ee559312ef8adf154cc539"
digest = "1:87f5f837cf3bee0aedbb43d1eeec6dd7d741a1e382d2f3b488fe8fa6c3486cbc"
name = "google.golang.org/api"
packages = [
"dns/v1",
@@ -2064,6 +2146,7 @@
"googleapi/transport",
"internal",
"option",
"support/bundler",
"transport/http",
"transport/http/internal/propagation",
]
@@ -2089,9 +2172,13 @@
revision = "4f7eeb5305a4ba1966344836ba4af9996b7b4e05"
[[projects]]
digest = "1:93180612a69db36a06d801302b867d53a50a8a5f0943b34db66adc0574ea57df"
digest = "1:a9d09c09842a02162edf759a35329c180e8ce82c2c6ae455a3c56d67b549be5b"
name = "google.golang.org/genproto"
packages = ["googleapis/rpc/status"]
packages = [
"googleapis/api/httpbody",
"googleapis/rpc/status",
"protobuf/field_mask",
]
pruneopts = "NUT"
revision = "09f6ed296fc66555a25fe4ce95173148778dfa85"
@@ -2185,7 +2272,15 @@
revision = "a5bcac82d3f637d3928d30476610891935b2d691"
[[projects]]
digest = "1:85a9c61c8d566fbbb108da8714893e6fc892fdb7a2910612a27f551ce30a0dd3"
digest = "1:0942599d1f614d9ca4dfe052db1f60d4547b1b581206006be352f629a8b37d8d"
name = "gopkg.in/resty.v1"
packages = ["."]
pruneopts = "NUT"
revision = "fa5875c0caa5c260ab78acec5a244215a730247f"
version = "v1.12.0"
[[projects]]
digest = "1:a56dc1133e9c449d41d22ffabd6c2343cd25ab31c9cad0e74a144006810d1544"
name = "gopkg.in/square/go-jose.v2"
packages = [
".",
@@ -2193,8 +2288,8 @@
"json",
]
pruneopts = "NUT"
revision = "6ee92191fea850cdcab9a18867abf5f521cdbadb"
version = "v2.1.4"
revision = "730df5f748271903322feb182be83b43ebbbe27d"
version = "v2.3.1"
[[projects]]
digest = "1:302ad18387350c3d9792da66de666f76d2ca8c62c47dd6b9434269c7cfa18971"

View File

@@ -180,7 +180,11 @@
[[constraint]]
name = "github.com/go-acme/lego"
version = "2.6.0"
version = "2.7.2"
[[constraint]]
name = "github.com/golang/protobuf"
version = "v1.3.0"
[[constraint]]
name = "google.golang.org/grpc"

View File

@@ -76,7 +76,7 @@ test-integration: build ## run the integration tests
TEST_HOST=1 ./script/make.sh test-integration
validate: build ## validate code, vendor and autogen
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-govet validate-golint validate-misspell validate-vendor validate-autogen
$(DOCKER_RUN_TRAEFIK) ./script/make.sh validate-gofmt validate-golint validate-misspell validate-vendor validate-autogen
build: dist
docker build $(DOCKER_BUILD_ARGS) -t "$(TRAEFIK_DEV_IMAGE)" -f build.Dockerfile .

View File

@@ -4,7 +4,7 @@
</p>
[![Build Status SemaphoreCI](https://semaphoreci.com/api/v1/containous/traefik/branches/master/shields_badge.svg)](https://semaphoreci.com/containous/traefik)
[![Docs](https://img.shields.io/badge/docs-current-brightgreen.svg)](https://docs.traefik.io)
[![Docs](https://img.shields.io/badge/docs-current-brightgreen.svg)](https://docs.traefik.io/v1.7)
[![Go Report Card](https://goreportcard.com/badge/containous/traefik)](http://goreportcard.com/report/containous/traefik)
[![](https://images.microbadger.com/badges/image/traefik.svg)](https://microbadger.com/images/traefik)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md)
@@ -70,22 +70,22 @@ _(But if you'd rather configure some of your routes manually, Traefik supports t
## Supported Backends
- [Docker](https://docs.traefik.io/configuration/backends/docker) / [Swarm mode](https://docs.traefik.io/configuration/backends/docker#docker-swarm-mode)
- [Kubernetes](https://docs.traefik.io/configuration/backends/kubernetes)
- [Mesos](https://docs.traefik.io/configuration/backends/mesos) / [Marathon](https://docs.traefik.io/configuration/backends/marathon)
- [Rancher](https://docs.traefik.io/configuration/backends/rancher) (API, Metadata)
- [Azure Service Fabric](https://docs.traefik.io/configuration/backends/servicefabric)
- [Consul Catalog](https://docs.traefik.io/configuration/backends/consulcatalog)
- [Consul](https://docs.traefik.io/configuration/backends/consul) / [Etcd](https://docs.traefik.io/configuration/backends/etcd) / [Zookeeper](https://docs.traefik.io/configuration/backends/zookeeper) / [BoltDB](https://docs.traefik.io/configuration/backends/boltdb)
- [Eureka](https://docs.traefik.io/configuration/backends/eureka)
- [Amazon ECS](https://docs.traefik.io/configuration/backends/ecs)
- [Amazon DynamoDB](https://docs.traefik.io/configuration/backends/dynamodb)
- [File](https://docs.traefik.io/configuration/backends/file)
- [Rest](https://docs.traefik.io/configuration/backends/rest)
- [Docker](https://docs.traefik.io/v1.7/configuration/backends/docker) / [Swarm mode](https://docs.traefik.io/v1.7/configuration/backends/docker#docker-swarm-mode)
- [Kubernetes](https://docs.traefik.io/v1.7/configuration/backends/kubernetes)
- [Mesos](https://docs.traefik.io/v1.7/configuration/backends/mesos) / [Marathon](https://docs.traefik.io/v1.7/configuration/backends/marathon)
- [Rancher](https://docs.traefik.io/v1.7/configuration/backends/rancher) (API, Metadata)
- [Azure Service Fabric](https://docs.traefik.io/v1.7/configuration/backends/servicefabric)
- [Consul Catalog](https://docs.traefik.io/v1.7/configuration/backends/consulcatalog)
- [Consul](https://docs.traefik.io/v1.7/configuration/backends/consul) / [Etcd](https://docs.traefik.io/v1.7/configuration/backends/etcd) / [Zookeeper](https://docs.traefik.io/v1.7/configuration/backends/zookeeper) / [BoltDB](https://docs.traefik.io/v1.7/configuration/backends/boltdb)
- [Eureka](https://docs.traefik.io/v1.7/configuration/backends/eureka)
- [Amazon ECS](https://docs.traefik.io/v1.7/configuration/backends/ecs)
- [Amazon DynamoDB](https://docs.traefik.io/v1.7/configuration/backends/dynamodb)
- [File](https://docs.traefik.io/v1.7/configuration/backends/file)
- [Rest](https://docs.traefik.io/v1.7/configuration/backends/rest)
## Quickstart
To get your hands on Traefik, you can use the [5-Minute Quickstart](http://docs.traefik.io/#the-traefik-quickstart-using-docker) in our documentation (you will need Docker).
To get your hands on Traefik, you can use the [5-Minute Quickstart](http://docs.traefik.io/v1.7/#the-traefik-quickstart-using-docker) in our documentation (you will need Docker).
Alternatively, if you don't want to install anything on your computer, you can try Traefik online in this great [Katacoda tutorial](https://www.katacoda.com/courses/traefik/deploy-load-balancer) that shows how to load balance requests between multiple Docker containers.
@@ -100,7 +100,7 @@ You can access the simple HTML frontend of Traefik.
## Documentation
You can find the complete documentation at [https://docs.traefik.io](https://docs.traefik.io).
You can find the complete documentation at [https://docs.traefik.io/v1.7](https://docs.traefik.io/v1.7).
A collection of contributions around Traefik can be found at [https://awesome.traefik.io](https://awesome.traefik.io).
## Support

View File

@@ -1365,7 +1365,9 @@ var _templatesKubernetesTmpl = []byte(`[backends]
{{if $frontend.Auth }}
[frontends."{{ $frontendName }}".auth]
headerField = "X-WebAuth-User"
{{if $frontend.Auth.HeaderField }}
headerField = "{{ $frontend.Auth.HeaderField }}"
{{end}}
{{if $frontend.Auth.Basic }}
[frontends."{{ $frontendName }}".auth.basic]

View File

@@ -1,4 +1,4 @@
FROM golang:1.11-alpine
FROM golang:1.12-alpine
RUN apk --update upgrade \
&& apk --no-cache --no-progress add git mercurial bash gcc musl-dev curl tar ca-certificates tzdata \
@@ -6,11 +6,10 @@ RUN apk --update upgrade \
&& rm -rf /var/cache/apk/*
RUN go get golang.org/x/lint/golint \
&& go get github.com/kisielk/errcheck \
&& go get github.com/client9/misspell/cmd/misspell
# Which docker version to test on
ARG DOCKER_VERSION=17.03.2
ARG DOCKER_VERSION=18.09.7
ARG DEP_VERSION=0.5.1
# Download go-bindata binary to bin folder in $GOPATH
@@ -25,7 +24,7 @@ RUN mkdir -p /usr/local/bin \
# Download docker
RUN mkdir -p /usr/local/bin \
&& curl -fL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}-ce.tgz \
&& curl -fL https://download.docker.com/linux/static/stable/x86_64/docker-${DOCKER_VERSION}.tgz \
| tar -xzC /usr/local/bin --transform 's#^.+/##x'
WORKDIR /go/src/github.com/containous/traefik

View File

@@ -111,6 +111,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration {
defaultConsulCatalog.Prefix = "traefik"
defaultConsulCatalog.FrontEndRule = "Host:{{.ServiceName}}.{{.Domain}}"
defaultConsulCatalog.Stale = false
defaultConsulCatalog.StrictChecks = true
// default Etcd
var defaultEtcd etcd.Provider

View File

@@ -352,14 +352,14 @@ func stats(globalConfiguration *configuration.GlobalConfiguration) {
Stats collection is enabled.
Many thanks for contributing to Traefik's improvement by allowing us to receive anonymous information from your configuration.
Help us improve Traefik by leaving this feature on :)
More details on: https://docs.traefik.io/basics/#collected-data
More details on: https://docs.traefik.io/v1.7/basics/#collected-data
`)
collect(globalConfiguration)
} else {
log.Info(`
Stats collection is disabled.
Help us improve Traefik by turning this feature on :)
More details on: https://docs.traefik.io/basics/#collected-data
More details on: https://docs.traefik.io/v1.7/basics/#collected-data
`)
}
}

View File

@@ -212,6 +212,12 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
}
}
// Thanks to SSLv3 being enabled by mistake in golang 1.12,
// If no minVersion is set, apply TLS1.0 as the minimum.
if entryPoint.TLS != nil && len(entryPoint.TLS.MinVersion) == 0 {
entryPoint.TLS.MinVersion = "VersionTLS10"
}
if entryPoint.TLS != nil && entryPoint.TLS.DefaultCertificate == nil && len(entryPoint.TLS.Certificates) > 0 {
log.Infof("No tls.defaultCertificate given for %s: using the first item in tls.certificates as a fallback.", entryPointName)
entryPoint.TLS.DefaultCertificate = &entryPoint.TLS.Certificates[0]

View File

@@ -12,6 +12,7 @@ import (
"github.com/containous/traefik/provider"
acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/provider/file"
"github.com/containous/traefik/tls"
"github.com/stretchr/testify/assert"
)
@@ -269,3 +270,69 @@ func TestInitACMEProvider(t *testing.T) {
})
}
}
func TestSetEffectiveConfigurationTLSMinVersion(t *testing.T) {
testCases := []struct {
desc string
provided EntryPoint
expected EntryPoint
}{
{
desc: "Entrypoint with no TLS",
provided: EntryPoint{
Address: ":80",
},
expected: EntryPoint{
Address: ":80",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
},
},
{
desc: "Entrypoint with TLS Specifying MinVersion",
provided: EntryPoint{
Address: ":443",
TLS: &tls.TLS{
MinVersion: "VersionTLS12",
},
},
expected: EntryPoint{
Address: ":443",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
TLS: &tls.TLS{
MinVersion: "VersionTLS12",
},
},
},
{
desc: "Entrypoint with TLS without Specifying MinVersion",
provided: EntryPoint{
Address: ":443",
TLS: &tls.TLS{},
},
expected: EntryPoint{
Address: ":443",
ForwardedHeaders: &ForwardedHeaders{Insecure: true},
TLS: &tls.TLS{
MinVersion: "VersionTLS10",
},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
gc := &GlobalConfiguration{
EntryPoints: map[string]*EntryPoint{
"foo": &test.provided,
},
}
gc.SetEffectiveConfiguration(defaultConfigFile)
assert.Equal(t, &test.expected, gc.EntryPoints["foo"])
})
}
}

View File

@@ -1,6 +1,6 @@
[Unit]
Description=Traefik
Documentation=https://docs.traefik.io
Documentation=https://docs.traefik.io/v1.7
#After=network-online.target
#AssertFileIsExecutable=/usr/bin/traefik
#AssertPathExists=/etc/traefik/traefik.toml

View File

@@ -279,7 +279,7 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
| Provider Name | Provider Code | Environment Variables | Wildcard & Root Domain Support |
|-------------------------------------------------------------|----------------|---------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------|
| [ACME DNS](https://github.com/joohoi/acme-dns) | `acme-dns` | `ACME_DNS_API_BASE`, `ACME_DNS_STORAGE_PATH` | Not tested yet |
| [Alibaba Cloud](https://www.vultr.com) | `alidns` | `ALICLOUD_ACCESS_KEY`, `ALICLOUD_SECRET_KEY`, `ALICLOUD_REGION_ID` | Not tested yet |
| [Alibaba Cloud](https://www.alibabacloud.com) | `alidns` | `ALICLOUD_ACCESS_KEY`, `ALICLOUD_SECRET_KEY`, `ALICLOUD_REGION_ID` | Not tested yet |
| [Auroradns](https://www.pcextreme.com/aurora/dns) | `auroradns` | `AURORA_USER_ID`, `AURORA_KEY`, `AURORA_ENDPOINT` | Not tested yet |
| [Azure](https://azure.microsoft.com/services/dns/) | `azure` | `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET`, `AZURE_SUBSCRIPTION_ID`, `AZURE_TENANT_ID`, `AZURE_RESOURCE_GROUP`, `[AZURE_METADATA_ENDPOINT]` | Not tested yet |
| [Bindman](https://github.com/labbsr0x/bindman-dns-webhook) | `bindman` | `BINDMAN_MANAGER_ADDRESS` | YES |
@@ -309,13 +309,14 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
| HTTP request | `httpreq` | `HTTPREQ_ENDPOINT`, `HTTPREQ_MODE`, `HTTPREQ_USERNAME`, `HTTPREQ_PASSWORD` (1) | YES |
| [IIJ](https://www.iij.ad.jp/) | `iij` | `IIJ_API_ACCESS_KEY`, `IIJ_API_SECRET_KEY`, `IIJ_DO_SERVICE_CODE` | Not tested yet |
| [INWX](https://www.inwx.de/en) | `inwx` | `INWX_USERNAME`, `INWX_PASSWORD` | YES |
| [Joker.com](https://joker.com) | `joker` | `JOKER_API_KEY` | YES |
| [Joker.com](https://joker.com) | `joker` | `JOKER_API_KEY` or `JOKER_USERNAME`, `JOKER_PASSWORD` | YES |
| [Lightsail](https://aws.amazon.com/lightsail/) | `lightsail` | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `DNS_ZONE` | Not tested yet |
| [Linode](https://www.linode.com) | `linode` | `LINODE_API_KEY` | Not tested yet |
| [Linode v4](https://www.linode.com) | `linodev4` | `LINODE_TOKEN` | Not tested yet |
| manual | - | none, but you need to run Traefik interactively, turn on `acmeLogging` to see instructions and press <kbd>Enter</kbd>. | YES |
| [MyDNS.jp](https://www.mydns.jp/) | `mydnsjp` | `MYDNSJP_MASTER_ID`, `MYDNSJP_PASSWORD` | YES |
| [Namecheap](https://www.namecheap.com) | `namecheap` | `NAMECHEAP_API_USER`, `NAMECHEAP_API_KEY` | YES |
| [Namesilo](https://www.namesilo.com/) | `namesilo` | `NAMESILO_API_KEY` | YES |
| [name.com](https://www.name.com/) | `namedotcom` | `NAMECOM_USERNAME`, `NAMECOM_API_TOKEN`, `NAMECOM_SERVER` | Not tested yet |
| [Netcup](https://www.netcup.eu/) | `netcup` | `NETCUP_CUSTOMER_NUMBER`, `NETCUP_API_KEY`, `NETCUP_API_PASSWORD` | Not tested yet |
| [NIFCloud](https://cloud.nifty.com/service/dns.htm) | `nifcloud` | `NIFCLOUD_ACCESS_KEY_ID`, `NIFCLOUD_SECRET_ACCESS_KEY` | Not tested yet |
@@ -333,8 +334,9 @@ For example, `CF_API_EMAIL_FILE=/run/secrets/traefik_cf-api-email` could be used
| [Stackpath](https://www.stackpath.com/) | `stackpath` | `STACKPATH_CLIENT_ID`, `STACKPATH_CLIENT_SECRET`, `STACKPATH_STACK_ID` | Not tested yet |
| [TransIP](https://www.transip.nl/) | `transip` | `TRANSIP_ACCOUNT_NAME`, `TRANSIP_PRIVATE_KEY_PATH` | YES |
| [VegaDNS](https://github.com/shupp/VegaDNS-API) | `vegadns` | `SECRET_VEGADNS_KEY`, `SECRET_VEGADNS_SECRET`, `VEGADNS_URL` | Not tested yet |
| [Versio](https://www.versio.nl/domeinnamen) | `versio` | `VERSIO_USERNAME`, `VERSIO_PASSWORD` | YES |
| [Vscale](https://vscale.io/) | `vscale` | `VSCALE_API_TOKEN` | YES |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | Not tested yet |
| [VULTR](https://www.vultr.com) | `vultr` | `VULTR_API_KEY` | YES |
| [Zone.ee](https://www.zone.ee) | `zoneee` | `ZONEEE_API_USER`, `ZONEEE_API_KEY` | YES |
- (1): more information about the HTTP message format can be found [here](https://go-acme.github.io/lego/dns/httpreq/)

View File

@@ -35,7 +35,7 @@
For more customization, see [entry points](/configuration/entrypoints/) documentation and the examples below.
## Web UI
## Dashboard (Web UI)
![Web UI Providers](/img/web.frontend.png)
@@ -322,9 +322,12 @@ curl -s "http://localhost:8080/health" | jq .
}
```
## Metrics
## Dashboard Statistics
You can enable Traefik to export internal metrics to different monitoring systems.
You can control how the Traefik's internal metrics are shown in the Dashboard.
If you want to export internal metrics to different monitoring systems,
please check the page [Metrics](./metrics.md).
```toml
[api]

View File

@@ -37,6 +37,15 @@ stale = false
#
domain = "consul.localhost"
# Keep a Consul node only if all checks status are passing
# If true, only the Consul nodes with checks status 'passing' will be kept.
# if false, only the Consul nodes with checks status 'passing' or 'warning' will be kept.
#
# Optional
# Default: true
#
strictChecks = true
# Prefix for Consul catalog tags.
#
# Optional
@@ -121,10 +130,10 @@ Additional settings can be defined using Consul Catalog tags.
| `<prefix>.frontend.auth.basic=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash` (DEPRECATED). |
| `<prefix>.frontend.auth.basic.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `<prefix>.frontend.auth.basic.users=EXPR` | Sets basic authentication to this frontend in CSV format: `User:Hash,User:Hash`. |
| `<prefix>.frontend.auth.basic.usersfile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.basic.usersFile=/path/.htpasswd` | Sets basic authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.digest.removeHeader=true` | If set to `true`, removes the `Authorization` header. |
| `<prefix>.frontend.auth.digest.users=EXPR` | Sets digest authentication to this frontend in CSV format: `User:Realm:Hash,User:Realm:Hash`. |
| `<prefix>.frontend.auth.digest.usersfile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.digest.usersFile=/path/.htdigest` | Sets digest authentication with an external file; if users and usersFile are provided, both are merged, with external file contents having precedence. |
| `<prefix>.frontend.auth.forward.address=https://example.com` | Sets the URL of the authentication server. |
| `<prefix>.frontend.auth.forward.authResponseHeaders=EXPR` | Sets the forward authentication authResponseHeaders in CSV format: `X-Auth-User,X-Auth-Header` |
| `<prefix>.frontend.auth.forward.tls.ca=/path/ca.pem` | Sets the Certificate Authority (CA) for the TLS connection with the authentication server. |

View File

@@ -73,6 +73,14 @@ See also [Kubernetes user guide](/user-guide/kubernetes).
#
# enablePassTLSCert = true
# Throttle how frequently we refresh our configuration from Ingresses when there
# are frequent changes.
#
# Optional
# Default: 0 (no throttling)
#
# throttleDuration = 10s
# Override default configuration template.
#
# Optional
@@ -210,10 +218,14 @@ infos:
serialnumber: true
```
If `pem` is set, it will add a `X-Forwarded-Tls-Client-Cert` header that contains the escaped pem as value.
If `pem` is set, it will add a `X-Forwarded-Tls-Client-Cert` header that contains the escaped pem as value.
If at least one flag of the `infos` part is set, it will add a `X-Forwarded-Tls-Client-Cert-Infos` header that contains an escaped string composed of the client certificate data selected by the infos flags.
This infos part is composed like the following example (not escaped):
```Subject="C=FR,ST=SomeState,L=Lyon,O=Cheese,CN=*.cheese.org",NB=1531900816,NA=1563436816,SAN=*.cheese.org,*.cheese.net,cheese.in,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2```
```
Subject="C=FR,ST=SomeState,L=Lyon,O=Cheese,CN=*.cheese.org",NB=1531900816,NA=1563436816,SAN=*.cheese.org,*.cheese.net,cheese.in,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2
```
Note these options work only with certificates issued by CAs included in the applicable [EntryPoint ClientCA section](/configuration/entrypoints/#tls-mutual-authentication); certificates from other CAs are not parsed or passed through as-is.
<4> `traefik.ingress.kubernetes.io/rate-limit` example:
@@ -231,7 +243,7 @@ rateset:
```
<5> `traefik.ingress.kubernetes.io/rule-type`
Note: `ReplacePath` is deprecated in this annotation, use the `traefik.ingress.kubernetes.io/request-modifier` annotation instead. Default: `PathPrefix`.
Note: `ReplacePath` is deprecated in this annotation, use the `traefik.ingress.kubernetes.io/request-modifier` annotation instead. Default: `PathPrefix`.
<6> `traefik.ingress.kubernetes.io/service-weights`:
Service weights enable to split traffic across multiple backing services in a fine-grained manner.

View File

@@ -2,6 +2,11 @@
Traefik can be configured to use Rancher as a provider.
!!! important
This provider is specific to Rancher 1.x.
Rancher 2.x requires Kubernetes and does not have a metadata endpoint of its own for Traefik to query.
As such, Rancher 2.x users should utilize the [Kubernetes provider](./kubernetes.md) directly.
## Global Configuration
```toml

View File

@@ -69,6 +69,8 @@ Here is an example of an extension setting Traefik labels:
</StatelessServiceType>
```
> **Note**: The `Label` tag and its `Key` attribute are case sensitive. That is, if you use `label` instead of `Label` or `key` instead of `Key`, they will be silently ignored.
#### Property Manager
Set Labels with the property manager API to overwrite and add labels, while your service is running.

View File

@@ -97,7 +97,7 @@ In compose file the entrypoint syntax is different. Notice how quotes are used:
```yaml
traefik:
image: traefik
image: traefik:v1.7
command:
- --defaultentrypoints=powpow
- "--entryPoints=Name:powpow Address::42 Compress:true"
@@ -105,7 +105,7 @@ traefik:
or
```yaml
traefik:
image: traefik
image: traefik:v1.7
command: --defaultentrypoints=powpow --entryPoints='Name:powpow Address::42 Compress:true'
```
@@ -235,8 +235,10 @@ If you need to add or remove TLS certificates while Traefik is started, Dynamic
## TLS Mutual Authentication
TLS Mutual Authentication can be `optional` or not.
If it's `optional`, Traefik will authorize connection with certificates not signed by a specified Certificate Authority (CA).
Otherwise, Traefik will only accept clients that present a certificate signed by a specified Certificate Authority (CA).
* If `optional = true`, if a certificate is provided, verifies if it is signed by a specified Certificate Authority (CA). Otherwise proceeds without any certificate.
* If `optional = false`, Traefik will only accept clients that present a certificate signed by a specified Certificate Authority (CA).
`ClientCAFiles` can be configured with multiple `CA:s` in the same file or use multiple files containing one or several `CA:s`.
The `CA:s` has to be in PEM format.

View File

@@ -39,7 +39,7 @@ logLevel = "INFO"
For more information about the CLI, see the documentation about [Traefik command](/basics/#traefik).
```shell
```bash
--logLevel="DEBUG"
--traefikLog.filePath="/path/to/traefik.log"
--traefikLog.format="json"
@@ -54,7 +54,6 @@ For more information about the CLI, see the documentation about [Traefik command
--accessLog.fields.headers.names="User-Agent=redact Authorization=drop Content-Type=keep"
```
## Traefik Logs
By default the Traefik log is written to stdout in text format.
@@ -66,7 +65,7 @@ To write the logs into a log file specify the `filePath`:
filePath = "/path/to/traefik.log"
```
To write JSON format logs, specify `json` as the format:
To switch to JSON format instead of standard format (`common`), specify `json` as the format:
```toml
[traefikLog]
@@ -74,7 +73,6 @@ To write JSON format logs, specify `json` as the format:
format = "json"
```
Deprecated way (before 1.4):
!!! danger "DEPRECATED"
@@ -105,13 +103,12 @@ To customize the log level:
logLevel = "ERROR"
```
## Access Logs
Access logs are written when `[accessLog]` is defined.
By default it will write to stdout and produce logs in the textual Common Log Format (CLF), extended with additional fields.
Access logs are written when the entry `[accessLog]` is defined (or the command line flag `--accesslog`).
By default it writes to stdout and produces logs in the textual [Common Log Format (CLF)](#clf-common-log-format), extended with additional fields.
To enable access logs using the default settings just add the `[accessLog]` entry:
To enable access logs using the default settings, add the `[accessLog]` entry in your `traefik.toml` configuration file:
```toml
[accessLog]
@@ -124,12 +121,12 @@ To write the logs into a log file specify the `filePath`:
filePath = "/path/to/access.log"
```
To write JSON format logs, specify `json` as the format:
To switch to JSON format instead of [Common Log Format (CLF)](#clf-common-log-format), specify `json` as the format:
```toml
[accessLog]
filePath = "/path/to/access.log"
format = "json"
format = "json" # Default: "common"
```
To write the logs in async, specify `bufferingSize` as the format (must be >0):
@@ -152,7 +149,7 @@ To filter logs you can specify a set of filters which are logically "OR-connecte
```toml
[accessLog]
filePath = "/path/to/access.log"
format = "json"
format = "json" # Default: "common"
[accessLog.filters]
@@ -178,22 +175,42 @@ format = "json"
minDuration = "10ms"
```
To customize logs format:
### CLF - Common Log Format
By default, Traefik use the CLF (`common`) as access log format.
```html
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_frontend_name>" "<Traefik_backend_URL>" <request_duration_in_ms>ms
```
### Customize Fields
You can customize the fields written in the access logs.
The list of available fields is found below: [List of All Available Fields](#list-of-all-available-fields).
Each field has a "mode" which defines if it is written or not in the access log lines.
The possible values for the mode are:
* `keep`: the field and its value are written on the access log line. This is the default behavior.
* `drop`: the field is not written at all on the access log.
To customize the fields, you must:
* Switch to the JSON format (mandatory)
* Define the "default mode" for all fields (default is `keep`)
* OR Define the fields which does not follow the default mode
```toml
[accessLog]
filePath = "/path/to/access.log"
# Access Log Format
#
# Optional
# Default: "common"
#
# Accepted values "common", "json"
#
format = "json"
[accessLog.filters]
# statusCodes keep only access logs with status codes in the specified range
#
# Optional
# Default: []
#
statusCodes = ["200", "300-302"]
[accessLog.fields]
# defaultMode
@@ -209,6 +226,43 @@ format = "json"
[accessLog.fields.names]
"ClientUsername" = "drop"
# ...
```
### Customize Headers
Access logs prints the headers of each request, as fields of the access log line.
You can customize which and how the headers are printed, likewise the other fields (see ["Customize Fields" section](#customize-fields)).
Each header has a "mode" which defines how it is written in the access log lines.
The possible values for the mode are:
* `keep`: the header and its value are written on the access log line. This is the default behavior.
* `drop`: the header is not written at all on the access log.
* `redacted`: the header is written, but its value is redacted to avoid leaking sensitive information.
To customize the headers, you must:
* Switch to the JSON format (mandatory)
* Define the "default mode" for all headers (default is `keep`)
* OR Define the headers which does not follow the default mode
!!! important
The headers are written with the prefix `request_` in the access log.
This prefix must not be included when specifying a header in the TOML configuration.
* Do: `"User-Agent" = "drop"`
* Don't: `"redacted_User-Agent" = "drop"`
```toml
[accessLog]
# Access Log Format
#
# Optional
# Default: "common"
#
# Accepted values "common", "json"
#
format = "json"
[accessLog.fields.headers]
# defaultMode
@@ -227,8 +281,7 @@ format = "json"
# ...
```
### List of all available fields
### List of All Available Fields
| Field | Description |
|-------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
@@ -263,6 +316,8 @@ format = "json"
| `Overhead` | The processing time overhead caused by Traefik. |
| `RetryAttempts` | The amount of attempts the request was retried. |
### Depreciation Notice
Deprecated way (before 1.4):
!!! danger "DEPRECATED"
@@ -276,15 +331,6 @@ Deprecated way (before 1.4):
accessLogsFile = "log/access.log"
```
### CLF - Common Log Format
By default, Traefik use the CLF (`common`) as access log format.
```html
<remote_IP_address> - <client_user_name_if_available> [<timestamp>] "<request_method> <request_path> <request_protocol>" <origin_server_HTTP_status> <origin_server_content_size> "<request_referrer>" "<request_user_agent>" <number_of_requests_received_since_Traefik_started> "<Traefik_frontend_name>" "<Traefik_backend_URL>" <request_duration_in_ms>ms
```
## Log Rotation
Traefik will close and reopen its log files, assuming they're configured, on receipt of a USR1 signal.
@@ -292,3 +338,34 @@ This allows the logs to be rotated and processed by an external program, such as
!!! note
This does not work on Windows due to the lack of USR signals.
## Time Zones
The timestamp of each log line is in UTC time by default.
If you want to use local timezone, you need to ensure the 3 following elements:
1. Provide the timezone data into /usr/share/zoneinfo
2. Set the environement variable TZ to the timezone to be used
3. Specify the field StartLocal instead of StartUTC (works on default Common Log Format (CLF) as well as JSON)
Example using docker-compose:
```yml
version: '3'
services:
traefik:
image: containous/traefik:[latest stable version]
ports:
- "80:80"
environment:
- "TZ=US/Alaska"
command:
- --docker
- --accesslog
- --accesslog.fields.names="StartUTC=drop"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "/usr/share/zoneinfo:/usr/share/zoneinfo:ro"
```

View File

@@ -6,7 +6,7 @@
[![Docs](https://img.shields.io/badge/docs-current-brightgreen.svg)](/)
[![Go Report Card](https://goreportcard.com/badge/github.com/containous/traefik)](https://goreportcard.com/report/github.com/containous/traefik)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/containous/traefik/blob/master/LICENSE.md)
[![Join the chat at https://slack.traefik.io](https://img.shields.io/badge/style-register-green.svg?style=social&label=Slack)](https://slack.traefik.io)
[![Join the community support forum at https://community.containo.us/](https://img.shields.io/badge/style-register-green.svg?style=social&label=Discourse)](https://community.containo.us/)
[![Twitter](https://img.shields.io/twitter/follow/traefik.svg?style=social)](https://twitter.com/intent/follow?screen_name=traefik)
@@ -77,7 +77,7 @@ version: '3'
services:
reverse-proxy:
image: traefik # The official Traefik docker image
image: traefik:v1.7 # The official Traefik docker image
command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports:
- "80:80" # The HTTP port

View File

@@ -91,7 +91,7 @@ To watch docker events, add `--docker.watch`.
version: "3"
services:
traefik:
image: traefik:1.5
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command:
- "--api"
- "--entrypoints=Name:http Address::80 Redirect.EntryPoint:https"
@@ -156,7 +156,7 @@ The initializer in a docker-compose file will be:
```yaml
traefik_init:
image: traefik:1.5
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command:
- "storeconfig"
- "--api"
@@ -177,7 +177,7 @@ And now, the Traefik part will only have the Consul configuration.
```yaml
traefik:
image: traefik:1.5
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
depends_on:
- traefik_init
- consul
@@ -200,7 +200,7 @@ The new configuration will be stored in Consul, and you need to restart the Trae
version: "3.4"
services:
traefik_init:
image: traefik:1.5
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command:
- "storeconfig"
- "--api"
@@ -229,7 +229,7 @@ services:
depends_on:
- consul
traefik:
image: traefik:1.5
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
depends_on:
- traefik_init
- consul

View File

@@ -50,7 +50,7 @@ version: '2'
services:
traefik:
image: traefik:1.5.4
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
restart: always
ports:
- 80:80
@@ -108,6 +108,28 @@ onHostRule = true
entryPoint = "http"
```
Alternatively, the `TOML` file above can also be translated into command line switches.
This is the `command` value of the `traefik` service in the `docker-compose.yml` manifest:
```yaml
command:
- --debug=false
- --logLevel=ERROR
- --defaultentrypoints=https,http
- --entryPoints=Name:http Address::80 Redirect.EntryPoint:https
- --entryPoints=Name:https Address::443 TLS
- --retry
- --docker.endpoint=unix:///var/run/docker.sock
- --docker.domain=my-awesome-app.org
- --docker.watch=true
- --docker.exposedbydefault=false
- --acme.email=your-email-here@my-awesome-app.org
- --acme.storage=acme.json
- --acme.entryPoint=https
- --acme.onHostRule=true
- --acme.httpchallenge.entrypoint=http
```
This is the minimum configuration required to do the following:
- Log `ERROR`-level messages (or more severe) to the console, but silence `DEBUG`-level messages

View File

@@ -336,37 +336,37 @@ Pay attention to the **labels** section:
```
home:
image: abiosoft/caddy:0.10.14
networks:
- ntw_front
volumes:
- ./www/home/srv/:/srv/
deploy:
mode: replicated
replicas: 2
#placement:
# constraints: [node.role==manager]
restart_policy:
condition: on-failure
max_attempts: 5
resources:
limits:
cpus: '0.20'
memory: 9M
reservations:
cpus: '0.05'
memory: 9M
labels:
- "traefik.frontend.rule=PathPrefixStrip:/"
- "traefik.backend=home"
- "traefik.port=2015"
- "traefik.weight=10"
- "traefik.enable=true"
- "traefik.passHostHeader=true"
- "traefik.docker.network=ntw_front"
- "traefik.frontend.entryPoints=http"
- "traefik.backend.loadbalancer.swarm=true"
- "traefik.backend.loadbalancer.method=drr"
image: abiosoft/caddy:0.10.14
networks:
- ntw_front
volumes:
- ./www/home/srv/:/srv/
deploy:
mode: replicated
replicas: 2
#placement:
# constraints: [node.role==manager]
restart_policy:
condition: on-failure
max_attempts: 5
resources:
limits:
cpus: '0.20'
memory: 9M
reservations:
cpus: '0.05'
memory: 9M
labels:
- "traefik.frontend.rule=PathPrefixStrip:/"
- "traefik.backend=home"
- "traefik.port=2015"
- "traefik.weight=10"
- "traefik.enable=true"
- "traefik.passHostHeader=true"
- "traefik.docker.network=ntw_front"
- "traefik.frontend.entryPoints=http"
- "traefik.backend.loadbalancer.swarm=true"
- "traefik.backend.loadbalancer.method=drr"
```
Something more tricky using `regex`.
@@ -377,39 +377,39 @@ The double sign `$$` are variables managed by the docker compose file ([document
```
portainer:
image: portainer/portainer:1.16.5
networks:
- ntw_front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role==manager]
restart_policy:
condition: on-failure
max_attempts: 5
resources:
limits:
cpus: '0.33'
memory: 20M
reservations:
cpus: '0.05'
memory: 10M
labels:
- "traefik.frontend.rule=PathPrefixStrip:/portainer"
- "traefik.backend=portainer"
- "traefik.port=9000"
- "traefik.weight=10"
- "traefik.enable=true"
- "traefik.passHostHeader=true"
- "traefik.docker.network=ntw_front"
- "traefik.frontend.entryPoints=http"
- "traefik.backend.loadbalancer.swarm=true"
- "traefik.backend.loadbalancer.method=drr"
# https://github.com/containous/traefik/issues/563#issuecomment-421360934
- "traefik.frontend.redirect.regex=^(.*)/portainer$$"
- "traefik.frontend.redirect.replacement=$$1/portainer/"
- "traefik.frontend.rule=PathPrefix:/portainer;ReplacePathRegex: ^/portainer/(.*) /$$1"
image: portainer/portainer:1.16.5
networks:
- ntw_front
volumes:
- /var/run/docker.sock:/var/run/docker.sock
deploy:
mode: replicated
replicas: 1
placement:
constraints: [node.role==manager]
restart_policy:
condition: on-failure
max_attempts: 5
resources:
limits:
cpus: '0.33'
memory: 20M
reservations:
cpus: '0.05'
memory: 10M
labels:
- "traefik.frontend.rule=PathPrefixStrip:/portainer"
- "traefik.backend=portainer"
- "traefik.port=9000"
- "traefik.weight=10"
- "traefik.enable=true"
- "traefik.passHostHeader=true"
- "traefik.docker.network=ntw_front"
- "traefik.frontend.entryPoints=http"
- "traefik.backend.loadbalancer.swarm=true"
- "traefik.backend.loadbalancer.method=drr"
# https://github.com/containous/traefik/issues/563#issuecomment-421360934
- "traefik.frontend.redirect.regex=^(.*)/portainer$$"
- "traefik.frontend.redirect.replacement=$$1/portainer/"
- "traefik.frontend.rule=PathPrefix:/portainer;ReplacePathRegex: ^/portainer/(.*) /$$1"
```

View File

@@ -118,7 +118,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http
@@ -180,7 +180,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http

View File

@@ -139,7 +139,7 @@ Here is the [docker-compose file](https://docs.docker.com/compose/compose-file/)
```yaml
traefik:
image: traefik
image: traefik:<stable v1.7 from https://hub.docker.com/_/traefik>
command: --consul --consul.endpoint=127.0.0.1:8500
ports:
- "80:80"

View File

@@ -85,7 +85,7 @@ docker-machine ssh manager "docker service create \
--publish 80:80 --publish 8080:8080 \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--network traefik-net \
traefik \
traefik:<stable version from https://hub.docker.com/_/traefik> \
--docker \
--docker.swarmMode \
--docker.domain=traefik \

View File

@@ -81,7 +81,7 @@ docker $(docker-machine config mhs-demo0) run \
-p 80:80 -p 8080:8080 \
--net=my-net \
-v /var/lib/boot2docker/:/ssl \
traefik \
traefik:<stable version from https://hub.docker.com/_/traefik> \
-l DEBUG \
-c /dev/null \
--docker \

View File

@@ -1,5 +1,5 @@
traefik:
image: traefik
image: traefik:v1.7
command: --api --rancher --rancher.domain=rancher.localhost --rancher.endpoint=http://example.com --rancher.accesskey=XXXXXXX --rancher.secretkey=YYYYYY --logLevel=DEBUG
ports:
- "80:80"

View File

@@ -1,5 +1,5 @@
traefik:
image: traefik
image: traefik:v1.7
command: -c /dev/null --api --docker --docker.domain=docker.localhost --logLevel=DEBUG
ports:
- "80:80"

View File

@@ -26,7 +26,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http

View File

@@ -22,7 +22,7 @@ spec:
serviceAccountName: traefik-ingress-controller
terminationGracePeriodSeconds: 60
containers:
- image: traefik
- image: traefik:v1.7
name: traefik-ingress-lb
ports:
- name: http

View File

@@ -13,7 +13,7 @@ version: '3'
services:
reverse-proxy:
image: traefik # The official Traefik docker image
image: traefik:v1.7 # The official Traefik docker image
command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports:
- "80:80" # The HTTP port
@@ -101,7 +101,7 @@ IP: 172.27.0.4
### 4 — Enjoy Traefik's Magic
Now that you have a basic understanding of how Traefik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](https://docs.traefik.io/) and let Traefik work for you!
Whatever your infrastructure is, there is probably [an available Traefik backend](https://docs.traefik.io/#supported-backends) that will do the job.
Now that you have a basic understanding of how Traefik can automatically create the routes to your services and load balance them, it might be time to dive into [the documentation](https://docs.traefik.io/v1.7/) and let Traefik work for you!
Whatever your infrastructure is, there is probably [an available Traefik backend](https://docs.traefik.io/v1.7/#supported-backends) that will do the job.
Our recommendation would be to see for yourself how simple it is to enable HTTPS with [Traefik's let's encrypt integration](https://docs.traefik.io/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](https://docs.traefik.io/user-guide/docker-and-lets-encrypt/).
Our recommendation would be to see for yourself how simple it is to enable HTTPS with [Traefik's let's encrypt integration](https://docs.traefik.io/v1.7/user-guide/examples/#lets-encrypt-support) using the dedicated [user guide](https://docs.traefik.io/v1.7/user-guide/docker-and-lets-encrypt/).

View File

@@ -3,7 +3,7 @@ version: '3'
services:
# The reverse proxy service (Traefik)
reverse-proxy:
image: traefik # The official Traefik docker image
image: traefik:v1.7 # The official Traefik docker image
command: --api --docker # Enables the web UI and tells Traefik to listen to docker
ports:
- "80:80" # The HTTP port

View File

@@ -12,7 +12,7 @@ RUN yarn install
RUN npm run build
# BUILD
FROM golang:1.11-alpine as gobuild
FROM golang:1.12-alpine as gobuild
RUN apk --update upgrade \
&& apk --no-cache --no-progress add git mercurial bash gcc musl-dev curl tar ca-certificates tzdata \

View File

@@ -49,11 +49,16 @@ func (opt Options) String() string {
return fmt.Sprintf("[Hostname: %s Headers: %v Path: %s Port: %d Interval: %s]", opt.Hostname, opt.Headers, opt.Path, opt.Port, opt.Interval)
}
type backendURL struct {
url *url.URL
weight int
}
// BackendConfig HealthCheck configuration for a backend
type BackendConfig struct {
Options
name string
disabledURLs []*url.URL
disabledURLs []backendURL
requestTimeout time.Duration
}
@@ -129,18 +134,18 @@ func (hc *HealthCheck) execute(ctx context.Context, backend *BackendConfig) {
func (hc *HealthCheck) checkBackend(backend *BackendConfig) {
enabledURLs := backend.LB.Servers()
var newDisabledURLs []*url.URL
for _, url := range backend.disabledURLs {
var newDisabledURLs []backendURL
for _, backendurl := range backend.disabledURLs {
serverUpMetricValue := float64(0)
if err := checkHealth(url, backend); err == nil {
log.Warnf("Health check up: Returning to server list. Backend: %q URL: %q", backend.name, url.String())
backend.LB.UpsertServer(url, roundrobin.Weight(1))
if err := checkHealth(backendurl.url, backend); err == nil {
log.Warnf("Health check up: Returning to server list. Backend: %q URL: %q Weight: %d", backend.name, backendurl.url.String(), backendurl.weight)
backend.LB.UpsertServer(backendurl.url, roundrobin.Weight(backendurl.weight))
serverUpMetricValue = 1
} else {
log.Warnf("Health check still failing. Backend: %q URL: %q Reason: %s", backend.name, url.String(), err)
newDisabledURLs = append(newDisabledURLs, url)
log.Warnf("Health check still failing. Backend: %q URL: %q Reason: %s", backend.name, backendurl.url.String(), err)
newDisabledURLs = append(newDisabledURLs, backendurl)
}
labelValues := []string{"backend", backend.name, "url", url.String()}
labelValues := []string{"backend", backend.name, "url", backendurl.url.String()}
hc.metrics.BackendServerUpGauge().With(labelValues...).Set(serverUpMetricValue)
}
backend.disabledURLs = newDisabledURLs
@@ -148,9 +153,18 @@ func (hc *HealthCheck) checkBackend(backend *BackendConfig) {
for _, url := range enabledURLs {
serverUpMetricValue := float64(1)
if err := checkHealth(url, backend); err != nil {
log.Warnf("Health check failed: Remove from server list. Backend: %q URL: %q Reason: %s", backend.name, url.String(), err)
weight := 1
rr, ok := backend.LB.(*roundrobin.RoundRobin)
if ok {
var gotWeight bool
weight, gotWeight = rr.ServerWeight(url)
if !gotWeight {
weight = 1
}
}
log.Warnf("Health check failed: Remove from server list. Backend: %q URL: %q Weight: %d Reason: %s", backend.name, url.String(), weight, err)
backend.LB.RemoveServer(url)
backend.disabledURLs = append(backend.disabledURLs, url)
backend.disabledURLs = append(backend.disabledURLs, backendURL{url, weight})
serverUpMetricValue = 0
}
labelValues := []string{"backend", backend.name, "url", url.String()}

View File

@@ -112,7 +112,7 @@ func TestSetBackendsConfiguration(t *testing.T) {
if test.startHealthy {
lb.servers = append(lb.servers, serverURL)
} else {
backend.disabledURLs = append(backend.disabledURLs, serverURL)
backend.disabledURLs = append(backend.disabledURLs, backendURL{serverURL, 1})
}
collectingMetrics := testhelpers.NewCollectingHealthCheckMetrics()

View File

@@ -3,6 +3,7 @@ package accesslog
import (
"context"
"fmt"
"io"
"net"
"net/http"
"net/url"
@@ -32,6 +33,19 @@ const (
JSONFormat = "json"
)
type noopCloser struct {
*os.File
}
func (n noopCloser) Write(p []byte) (int, error) {
return n.File.Write(p)
}
func (n noopCloser) Close() error {
// noop
return nil
}
type logHandlerParams struct {
logDataTable *LogData
crr *captureRequestReader
@@ -42,7 +56,7 @@ type logHandlerParams struct {
type LogHandler struct {
config *types.AccessLog
logger *logrus.Logger
file *os.File
file io.WriteCloser
mu sync.Mutex
httpCodeRanges types.HTTPCodeRanges
logHandlerChan chan logHandlerParams
@@ -51,7 +65,7 @@ type LogHandler struct {
// NewLogHandler creates a new LogHandler
func NewLogHandler(config *types.AccessLog) (*LogHandler, error) {
file := os.Stdout
var file io.WriteCloser = noopCloser{os.Stdout}
if len(config.FilePath) > 0 {
f, err := openAccessLogFile(config.FilePath)
if err != nil {
@@ -205,14 +219,15 @@ func (l *LogHandler) Close() error {
// Rotate closes and reopens the log file to allow for rotation
// by an external source.
func (l *LogHandler) Rotate() error {
var err error
if l.file != nil {
defer func(f *os.File) {
f.Close()
}(l.file)
if l.config.FilePath == "" {
return nil
}
if l.file != nil {
defer func(f io.Closer) { _ = f.Close() }(l.file)
}
var err error
l.file, err = os.OpenFile(l.config.FilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0664)
if err != nil {
return err

View File

@@ -24,6 +24,8 @@ func (f *CommonLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
var timestamp = defaultValue
if v, ok := entry.Data[StartUTC]; ok {
timestamp = v.(time.Time).Format(commonLogTimeFormat)
} else if v, ok := entry.Data[StartLocal]; ok {
timestamp = v.(time.Time).Local().Format(commonLogTimeFormat)
}
var elapsedMillis int64

View File

@@ -2,6 +2,7 @@ package accesslog
import (
"net/http"
"os"
"testing"
"time"
@@ -57,10 +58,34 @@ func TestCommonLogFormatter_Format(t *testing.T) {
BackendURL: "http://10.0.0.2/toto",
},
expectedLog: `10.0.0.1 - Client [10/Nov/2009:23:00:00 +0000] "GET /foo http" 123 132 "referer" "agent" - "foo" "http://10.0.0.2/toto" 123000ms
`,
},
{
name: "all data with local time",
data: map[string]interface{}{
StartLocal: time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC),
Duration: 123 * time.Second,
ClientHost: "10.0.0.1",
ClientUsername: "Client",
RequestMethod: http.MethodGet,
RequestPath: "/foo",
RequestProtocol: "http",
OriginStatus: 123,
OriginContentSize: 132,
RequestRefererHeader: "referer",
RequestUserAgentHeader: "agent",
RequestCount: nil,
FrontendName: "foo",
BackendURL: "http://10.0.0.2/toto",
},
expectedLog: `10.0.0.1 - Client [10/Nov/2009:14:00:00 -0900] "GET /foo http" 123 132 "referer" "agent" - "foo" "http://10.0.0.2/toto" 123000ms
`,
},
}
// Set timezone to Alaska to have a constant behavior
os.Setenv("TZ", "US/Alaska")
for _, test := range testCases {
test := test
t.Run(test.name, func(t *testing.T) {

View File

@@ -19,7 +19,10 @@ import (
)
// Compile time validation that the response recorder implements http interfaces correctly.
var _ middlewares.Stateful = &responseRecorderWithCloseNotify{}
var (
_ middlewares.Stateful = &responseRecorderWithCloseNotify{}
_ middlewares.Stateful = &codeCatcherWithCloseNotify{}
)
// Handler is a middleware that provides the custom error pages
type Handler struct {
@@ -74,25 +77,29 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
return
}
recorder := newResponseRecorder(w)
next.ServeHTTP(recorder, req)
catcher := newCodeCatcher(w, h.httpCodeRanges)
next.ServeHTTP(catcher, req)
if !catcher.isFilteredCode() {
return
}
// check the recorder code against the configured http status code ranges
code := catcher.getCode()
for _, block := range h.httpCodeRanges {
if recorder.GetCode() >= block[0] && recorder.GetCode() <= block[1] {
log.Errorf("Caught HTTP Status Code %d, returning error page", recorder.GetCode())
if code >= block[0] && code <= block[1] {
log.Errorf("Caught HTTP Status Code %d, returning error page", code)
var query string
if len(h.backendQuery) > 0 {
query = "/" + strings.TrimPrefix(h.backendQuery, "/")
query = strings.Replace(query, "{status}", strconv.Itoa(recorder.GetCode()), -1)
query = strings.Replace(query, "{status}", strconv.Itoa(code), -1)
}
pageReq, err := newRequest(h.backendURL + query)
if err != nil {
log.Error(err)
w.WriteHeader(recorder.GetCode())
fmt.Fprint(w, http.StatusText(recorder.GetCode()))
w.WriteHeader(code)
fmt.Fprint(w, http.StatusText(code))
return
}
@@ -102,16 +109,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.
h.backendHandler.ServeHTTP(recorderErrorPage, pageReq.WithContext(req.Context()))
utils.CopyHeaders(w.Header(), recorderErrorPage.Header())
w.WriteHeader(recorder.GetCode())
w.WriteHeader(code)
w.Write(recorderErrorPage.GetBody().Bytes())
return
}
}
// did not catch a configured status code so proceed with the request
utils.CopyHeaders(w.Header(), recorder.Header())
w.WriteHeader(recorder.GetCode())
w.Write(recorder.GetBody().Bytes())
}
func newRequest(baseURL string) (*http.Request, error) {
@@ -129,6 +131,133 @@ func newRequest(baseURL string) (*http.Request, error) {
return req, nil
}
type responseInterceptor interface {
http.ResponseWriter
http.Flusher
getCode() int
isFilteredCode() bool
}
// codeCatcher is a response writer that detects as soon as possible whether the
// response is a code within the ranges of codes it watches for. If it is, it
// simply drops the data from the response. Otherwise, it forwards it directly to
// the original client (its responseWriter) without any buffering.
type codeCatcher struct {
headerMap http.Header
code int
httpCodeRanges types.HTTPCodeRanges
firstWrite bool
caughtFilteredCode bool
responseWriter http.ResponseWriter
headersSent bool
err error
}
type codeCatcherWithCloseNotify struct {
*codeCatcher
}
// CloseNotify returns a channel that receives at most a
// single value (true) when the client connection has gone away.
func (cc *codeCatcherWithCloseNotify) CloseNotify() <-chan bool {
return cc.responseWriter.(http.CloseNotifier).CloseNotify()
}
func newCodeCatcher(rw http.ResponseWriter, httpCodeRanges types.HTTPCodeRanges) responseInterceptor {
catcher := &codeCatcher{
headerMap: make(http.Header),
code: http.StatusOK, // If backend does not call WriteHeader on us, we consider it's a 200.
responseWriter: rw,
httpCodeRanges: httpCodeRanges,
firstWrite: true,
}
if _, ok := rw.(http.CloseNotifier); ok {
return &codeCatcherWithCloseNotify{catcher}
}
return catcher
}
func (cc *codeCatcher) Header() http.Header {
if cc.headerMap == nil {
cc.headerMap = make(http.Header)
}
return cc.headerMap
}
func (cc *codeCatcher) getCode() int {
return cc.code
}
// isFilteredCode returns whether the codeCatcher received a response code among the ones it is watching,
// and for which the response should be deferred to the error handler.
func (cc *codeCatcher) isFilteredCode() bool {
return cc.caughtFilteredCode
}
func (cc *codeCatcher) Write(buf []byte) (int, error) {
if !cc.firstWrite {
if cc.caughtFilteredCode {
// We don't care about the contents of the response,
// since we want to serve the ones from the error page,
// so we just drop them.
return len(buf), nil
}
return cc.responseWriter.Write(buf)
}
cc.firstWrite = false
// If WriteHeader was already called from the caller, this is a NOOP.
// Otherwise, cc.code is actually a 200 here.
cc.WriteHeader(cc.code)
if cc.caughtFilteredCode {
return len(buf), nil
}
return cc.responseWriter.Write(buf)
}
func (cc *codeCatcher) WriteHeader(code int) {
if cc.headersSent || cc.caughtFilteredCode {
return
}
cc.code = code
for _, block := range cc.httpCodeRanges {
if cc.code >= block[0] && cc.code <= block[1] {
cc.caughtFilteredCode = true
break
}
}
// it will be up to the other response recorder to send the headers,
// so it is out of our hands now.
if cc.caughtFilteredCode {
return
}
utils.CopyHeaders(cc.responseWriter.Header(), cc.Header())
cc.responseWriter.WriteHeader(cc.code)
cc.headersSent = true
}
// Hijack hijacks the connection
func (cc *codeCatcher) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if hj, ok := cc.responseWriter.(http.Hijacker); ok {
return hj.Hijack()
}
return nil, nil, fmt.Errorf("%T is not a http.Hijacker", cc.responseWriter)
}
// Flush sends any buffered data to the client.
func (cc *codeCatcher) Flush() {
// If WriteHeader was already called from the caller, this is a NOOP.
// Otherwise, cc.code is actually a 200 here.
cc.WriteHeader(cc.code)
if flusher, ok := cc.responseWriter.(http.Flusher); ok {
flusher.Flush()
}
}
type responseRecorder interface {
http.ResponseWriter
http.Flusher

View File

@@ -34,6 +34,30 @@ func TestHandler(t *testing.T) {
assert.Contains(t, recorder.Body.String(), http.StatusText(http.StatusOK))
},
},
{
desc: "no error, but not a 200",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
backendCode: http.StatusPartialContent,
backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "My error page.")
}),
validate: func(t *testing.T, recorder *httptest.ResponseRecorder) {
assert.Equal(t, http.StatusPartialContent, recorder.Code, "HTTP status")
assert.Contains(t, recorder.Body.String(), http.StatusText(http.StatusPartialContent))
},
},
{
desc: "a 304, so no Write called",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
backendCode: http.StatusNotModified,
backendErrorHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "whatever, should not be called")
}),
validate: func(t *testing.T, recorder *httptest.ResponseRecorder) {
assert.Equal(t, http.StatusNotModified, recorder.Code, "HTTP status")
assert.Contains(t, recorder.Body.String(), "")
},
},
{
desc: "in the range",
errorPage: &types.ErrorPage{Backend: "error", Query: "/test", Status: []string{"500-501", "503-599"}},
@@ -108,6 +132,9 @@ func TestHandler(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(test.backendCode)
if test.backendCode == http.StatusNotModified {
return
}
fmt.Fprintln(w, http.StatusText(test.backendCode))
})

View File

@@ -5,6 +5,7 @@ import (
"net/http"
"net/http/httptest"
"net/http/httptrace"
"strconv"
"strings"
"testing"
@@ -35,7 +36,7 @@ func TestRetry(t *testing.T) {
desc: "no retry when max request attempts is one",
maxRequestAttempts: 1,
wantRetryAttempts: 0,
wantResponseStatus: http.StatusInternalServerError,
wantResponseStatus: http.StatusBadGateway,
amountFaultyEndpoints: 1,
},
{
@@ -56,7 +57,7 @@ func TestRetry(t *testing.T) {
desc: "max attempts exhausted delivers the 5xx response",
maxRequestAttempts: 3,
wantRetryAttempts: 2,
wantResponseStatus: http.StatusInternalServerError,
wantResponseStatus: http.StatusBadGateway,
amountFaultyEndpoints: 3,
},
}
@@ -82,17 +83,18 @@ func TestRetry(t *testing.T) {
t.Fatalf("Error creating load balancer: %s", err)
}
basePort := 33444
// out of range port
basePort := 1133444
for i := 0; i < test.amountFaultyEndpoints; i++ {
// 192.0.2.0 is a non-routable IP for testing purposes.
// See: https://stackoverflow.com/questions/528538/non-routable-ip-address/18436928#18436928
// We only use the port specification here because the URL is used as identifier
// in the load balancer and using the exact same URL would not add a new server.
loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + string(basePort+i)))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + strconv.Itoa(basePort+i)))
}
// add the functioning server to the end of the load balancer list
loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
retryListener := &countingRetryListener{}
retry := NewRetry(test.maxRequestAttempts, loadBalancer, retryListener)
@@ -154,17 +156,18 @@ func TestRetryWebsocket(t *testing.T) {
t.Fatalf("Error creating load balancer: %s", err)
}
basePort := 33444
// out of range port
basePort := 1133444
for i := 0; i < test.amountFaultyEndpoints; i++ {
// 192.0.2.0 is a non-routable IP for testing purposes.
// See: https://stackoverflow.com/questions/528538/non-routable-ip-address/18436928#18436928
// We only use the port specification here because the URL is used as identifier
// in the load balancer and using the exact same URL would not add a new server.
loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + string(basePort+i)))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL("http://192.0.2.0:" + strconv.Itoa(basePort+i)))
}
// add the functioning server to the end of the load balancer list
loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
_ = loadBalancer.UpsertServer(testhelpers.MustParseURL(backendServer.URL))
retryListener := &countingRetryListener{}
retry := NewRetry(test.maxRequestAttempts, loadBalancer, retryListener)

View File

@@ -44,7 +44,7 @@ type DistinguishedNameOptions struct {
// TLSClientHeaders is a middleware that helps setup a few tls info features.
type TLSClientHeaders struct {
Infos *TLSClientCertificateInfos // pass selected informations from the client certificate
Infos *TLSClientCertificateInfos // pass selected information from the client certificate
PEM bool // pass the sanitized pem to the backend in a specific header
}
@@ -79,23 +79,14 @@ func newTLSClientInfos(infos *types.TLSClientCertificateInfos) *TLSClientCertifi
}
// NewTLSClientHeaders constructs a new TLSClientHeaders instance from supplied frontend header struct.
func NewTLSClientHeaders(frontend *types.Frontend) *TLSClientHeaders {
if frontend == nil {
func NewTLSClientHeaders(passTLSClientCert *types.TLSClientHeaders) *TLSClientHeaders {
if passTLSClientCert == nil {
return nil
}
var addPEM bool
var infos *TLSClientCertificateInfos
if frontend.PassTLSClientCert != nil {
conf := frontend.PassTLSClientCert
addPEM = conf.PEM
infos = newTLSClientInfos(conf.Infos)
}
return &TLSClientHeaders{
Infos: infos,
PEM: addPEM,
Infos: newTLSClientInfos(passTLSClientCert.Infos),
PEM: passTLSClientCert.PEM,
}
}
@@ -221,7 +212,7 @@ func writePart(content *strings.Builder, entry string, prefix string) {
}
}
// getXForwardedTLSClientCertInfo Build a string with the wanted client certificates informations
// getXForwardedTLSClientCertInfo Build a string with the wanted client certificates information
// like Subject="DC=%s,C=%s,ST=%s,L=%s,O=%s,CN=%s",NB=%d,NA=%d,SAN=%s;
func (s *TLSClientHeaders) getXForwardedTLSClientCertInfo(certs []*x509.Certificate) string {
var headerValues []string
@@ -268,8 +259,9 @@ func (s *TLSClientHeaders) getXForwardedTLSClientCertInfo(certs []*x509.Certific
return strings.Join(headerValues, ";")
}
// ModifyRequestHeaders set the wanted headers with the certificates informations
// ModifyRequestHeaders set the wanted headers with the certificates information
func (s *TLSClientHeaders) ModifyRequestHeaders(r *http.Request) {
r.Header.Del(xForwardedTLSClientCert)
if s.PEM {
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
r.Header.Set(xForwardedTLSClientCert, getXForwardedTLSClientCert(r.TLS.PeerCertificates))
@@ -278,6 +270,7 @@ func (s *TLSClientHeaders) ModifyRequestHeaders(r *http.Request) {
}
}
r.Header.Del(xForwardedTLSClientCertInfos)
if s.Infos != nil {
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
headerContent := s.getXForwardedTLSClientCertInfo(r.TLS.PeerCertificates)

View File

@@ -240,26 +240,6 @@ mxcl71pV8i3NDU3kgVi2440JYpoMveTlXPCV2svHNCw0X238YHsSW4b93yGJO0gI
ML9n/4zmm1PMhzZHcEA72ZAq0tKCxpz10djg5v2qL5V+Oaz8TtTOZbPsxpiKMQ==
-----END CERTIFICATE-----
`
minimalCert = `-----BEGIN CERTIFICATE-----
MIIDGTCCAgECCQCqLd75YLi2kDANBgkqhkiG9w0BAQsFADBYMQswCQYDVQQGEwJG
UjETMBEGA1UECAwKU29tZS1TdGF0ZTERMA8GA1UEBwwIVG91bG91c2UxITAfBgNV
BAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0xODA3MTgwODI4MTZaFw0x
ODA4MTcwODI4MTZaMEUxCzAJBgNVBAYTAkZSMRMwEQYDVQQIDApTb21lLVN0YXRl
MSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3
DQEBAQUAA4IBDwAwggEKAoIBAQC/+frDMMTLQyXG34F68BPhQq0kzK4LIq9Y0/gl
FjySZNn1C0QDWA1ubVCAcA6yY204I9cxcQDPNrhC7JlS5QA8Y5rhIBrqQlzZizAi
Rj3NTrRjtGUtOScnHuJaWjLy03DWD+aMwb7q718xt5SEABmmUvLwQK+EjW2MeDwj
y8/UEIpvrRDmdhGaqv7IFpIDkcIF7FowJ/hwDvx3PMc+z/JWK0ovzpvgbx69AVbw
ZxCimeha65rOqVi+lEetD26le+WnOdYsdJ2IkmpPNTXGdfb15xuAc+gFXfMCh7Iw
3Ynl6dZtZM/Ok2kiA7/OsmVnRKkWrtBfGYkI9HcNGb3zrk6nAgMBAAEwDQYJKoZI
hvcNAQELBQADggEBAC/R+Yvhh1VUhcbK49olWsk/JKqfS3VIDQYZg1Eo+JCPbwgS
I1BSYVfMcGzuJTX6ua3m/AHzGF3Tap4GhF4tX12jeIx4R4utnjj7/YKkTvuEM2f4
xT56YqI7zalGScIB0iMeyNz1QcimRl+M/49au8ow9hNX8C2tcA2cwd/9OIj/6T8q
SBRHc6ojvbqZSJCO0jziGDT1L3D+EDgTjED4nd77v/NRdP+egb0q3P0s4dnQ/5AV
aQlQADUn61j3ScbGJ4NSeZFFvsl38jeRi/MEzp0bGgNBcPj6JHi7qbbauZcZfQ05
jECvgAY7Nfd9mZ1KtyNaW31is+kag7NsvjxU/kM=
-----END CERTIFICATE-----`
)
func getCleanCertContents(certContents []string) string {
@@ -303,10 +283,6 @@ func buildTLSWith(certContents []string) *tls.ConnectionState {
return &tls.ConnectionState{PeerCertificates: peerCertificates}
}
var myPassTLSClientCustomHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("bar"))
})
func getExpectedSanitized(s string) string {
return url.QueryEscape(strings.Replace(s, "\n", "", -1))
}
@@ -360,20 +336,13 @@ WqeUSNGYV//RunTeuRDAf5OxehERb1srzBXhRZ3cZdzXbgR/`),
}
func TestTlsClientheadersWithPEM(t *testing.T) {
func TestTlsClientHeadersWithPEM(t *testing.T) {
testCases := []struct {
desc string
certContents []string // set the request TLS attribute if defined
tlsClientCertHeaders *types.TLSClientHeaders
expectedHeader string
}{
{
desc: "No TLS, no option",
},
{
desc: "TLS, no option",
certContents: []string{minimalCheeseCrt},
},
{
desc: "No TLS, with pem option true",
tlsClientCertHeaders: &types.TLSClientHeaders{PEM: true},
@@ -399,20 +368,24 @@ func TestTlsClientheadersWithPEM(t *testing.T) {
}
for _, test := range testCases {
tlsClientHeaders := NewTLSClientHeaders(&types.Frontend{PassTLSClientCert: test.tlsClientCertHeaders})
res := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil)
if test.certContents != nil && len(test.certContents) > 0 {
req.TLS = buildTLSWith(test.certContents)
}
tlsClientHeaders.ServeHTTP(res, req, myPassTLSClientCustomHandler)
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
tlsClientHeaders := NewTLSClientHeaders(test.tlsClientCertHeaders)
res := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil)
if test.tlsClientCertHeaders != nil {
req.Header.Set(xForwardedTLSClientCert, "Unsanitized HEADER")
}
if test.certContents != nil && len(test.certContents) > 0 {
req.TLS = buildTLSWith(test.certContents)
}
tlsClientHeaders.ServeHTTP(res, req, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("bar"))
}))
require.Equal(t, http.StatusOK, res.Code, "Http Status should be OK")
require.Equal(t, "bar", res.Body.String(), "Should be the expected body")
@@ -477,7 +450,7 @@ func TestGetSans(t *testing.T) {
}
func TestTlsClientheadersWithCertInfos(t *testing.T) {
func TestTlsClientHeadersWithCertInfos(t *testing.T) {
minimalCheeseCertAllInfos := `Subject="C=FR,ST=Some-State,O=Cheese",Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2",NB=1544094636,NA=1632568236,SAN=`
completeCertAllInfos := `Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=*.cheese.com",Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2",NB=1544094616,NA=1607166616,SAN=*.cheese.org,*.cheese.net,*.cheese.com,test@cheese.org,test@cheese.net,10.0.1.0,10.0.1.2`
@@ -487,13 +460,6 @@ func TestTlsClientheadersWithCertInfos(t *testing.T) {
tlsClientCertHeaders *types.TLSClientHeaders
expectedHeader string
}{
{
desc: "No TLS, no option",
},
{
desc: "TLS, no option",
certContents: []string{minimalCert},
},
{
desc: "No TLS, with pem option true",
tlsClientCertHeaders: &types.TLSClientHeaders{
@@ -627,20 +593,24 @@ func TestTlsClientheadersWithCertInfos(t *testing.T) {
},
}
for _, test := range testCases {
tlsClientHeaders := NewTLSClientHeaders(&types.Frontend{PassTLSClientCert: test.tlsClientCertHeaders})
res := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil)
if test.certContents != nil && len(test.certContents) > 0 {
req.TLS = buildTLSWith(test.certContents)
}
tlsClientHeaders.ServeHTTP(res, req, myPassTLSClientCustomHandler)
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
tlsClientHeaders := NewTLSClientHeaders(test.tlsClientCertHeaders)
res := httptest.NewRecorder()
req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil)
if test.tlsClientCertHeaders != nil {
req.Header.Set(xForwardedTLSClientCertInfos, "Unsanitized HEADER")
}
if test.certContents != nil && len(test.certContents) > 0 {
req.TLS = buildTLSWith(test.certContents)
}
tlsClientHeaders.ServeHTTP(res, req, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("bar"))
}))
require.Equal(t, http.StatusOK, res.Code, "Http Status should be OK")
require.Equal(t, "bar", res.Body.String(), "Should be the expected body")
@@ -664,45 +634,31 @@ func TestTlsClientheadersWithCertInfos(t *testing.T) {
func TestNewTLSClientHeadersFromStruct(t *testing.T) {
testCases := []struct {
desc string
frontend *types.Frontend
expected *TLSClientHeaders
desc string
tlsClientHeaders *types.TLSClientHeaders
expected *TLSClientHeaders
}{
{
desc: "Without frontend",
},
{
desc: "frontend without the option",
frontend: &types.Frontend{},
expected: &TLSClientHeaders{},
},
{
desc: "frontend with the pem set false",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
PEM: false,
},
desc: "TLS client headers with the pem set false",
tlsClientHeaders: &types.TLSClientHeaders{
PEM: false,
},
expected: &TLSClientHeaders{PEM: false},
},
{
desc: "frontend with the pem set true",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
PEM: true,
},
desc: "TLS client headers with the pem set true",
tlsClientHeaders: &types.TLSClientHeaders{
PEM: true,
},
expected: &TLSClientHeaders{PEM: true},
},
{
desc: "frontend with the Infos with no flag",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: false,
NotBefore: false,
Sans: false,
},
desc: "TLS client headers with the Infos with no flag",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: false,
NotBefore: false,
Sans: false,
},
},
expected: &TLSClientHeaders{
@@ -711,14 +667,12 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos basic",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: true,
NotBefore: true,
Sans: true,
},
desc: "TLS client headers with the Infos basic",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: true,
NotBefore: true,
Sans: true,
},
},
expected: &TLSClientHeaders{
@@ -731,12 +685,10 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos NotAfter",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: true,
},
desc: "TLS client headers with the Infos NotAfter",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: true,
},
},
expected: &TLSClientHeaders{
@@ -747,12 +699,10 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos NotBefore",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotBefore: true,
},
desc: "TLS client headers with the Infos NotBefore",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotBefore: true,
},
},
expected: &TLSClientHeaders{
@@ -763,12 +713,10 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Sans",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Sans: true,
},
desc: "TLS client headers with the Infos Sans",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Sans: true,
},
},
expected: &TLSClientHeaders{
@@ -779,13 +727,11 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Subject Organization",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Organization: true,
},
desc: "TLS client headers with the Infos Subject Organization",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Organization: true,
},
},
},
@@ -799,13 +745,11 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Subject Country",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Country: true,
},
desc: "TLS client headers with the Infos Subject Country",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Country: true,
},
},
},
@@ -819,13 +763,11 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Subject SerialNumber",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
SerialNumber: true,
},
desc: "TLS client headers with the Infos Subject SerialNumber",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
SerialNumber: true,
},
},
},
@@ -839,13 +781,11 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Subject Province",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Province: true,
},
desc: "TLS client headers with the Infos Subject Province",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Province: true,
},
},
},
@@ -859,13 +799,11 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Subject Locality",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Locality: true,
},
desc: "TLS client headers with the Infos Subject Locality",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
Locality: true,
},
},
},
@@ -879,13 +817,11 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Subject CommonName",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
CommonName: true,
},
desc: "TLS client headers with the Infos Subject CommonName",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Subject: &types.TLSCLientCertificateDNInfos{
CommonName: true,
},
},
},
@@ -899,19 +835,17 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos Issuer",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Issuer: &types.TLSCLientCertificateDNInfos{
CommonName: true,
Country: true,
DomainComponent: true,
Locality: true,
Organization: true,
SerialNumber: true,
Province: true,
},
desc: "TLS client headers with the Infos Issuer",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Issuer: &types.TLSCLientCertificateDNInfos{
CommonName: true,
Country: true,
DomainComponent: true,
Locality: true,
Organization: true,
SerialNumber: true,
Province: true,
},
},
},
@@ -931,12 +865,10 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Sans Infos",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Sans: true,
},
desc: "TLS client headers with the Sans Infos",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
Sans: true,
},
},
expected: &TLSClientHeaders{
@@ -947,30 +879,28 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
},
},
{
desc: "frontend with the Infos all",
frontend: &types.Frontend{
PassTLSClientCert: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: true,
NotBefore: true,
Subject: &types.TLSCLientCertificateDNInfos{
CommonName: true,
Country: true,
Locality: true,
Organization: true,
Province: true,
SerialNumber: true,
},
Issuer: &types.TLSCLientCertificateDNInfos{
Country: true,
DomainComponent: true,
Locality: true,
Organization: true,
SerialNumber: true,
Province: true,
},
Sans: true,
desc: "TLS client headers with the Infos all",
tlsClientHeaders: &types.TLSClientHeaders{
Infos: &types.TLSClientCertificateInfos{
NotAfter: true,
NotBefore: true,
Subject: &types.TLSCLientCertificateDNInfos{
CommonName: true,
Country: true,
Locality: true,
Organization: true,
Province: true,
SerialNumber: true,
},
Issuer: &types.TLSCLientCertificateDNInfos{
Country: true,
DomainComponent: true,
Locality: true,
Organization: true,
SerialNumber: true,
Province: true,
},
Sans: true,
},
},
expected: &TLSClientHeaders{
@@ -1004,8 +934,7 @@ func TestNewTLSClientHeadersFromStruct(t *testing.T) {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
require.Equal(t, test.expected, NewTLSClientHeaders(test.frontend))
require.Equal(t, test.expected, NewTLSClientHeaders(test.tlsClientHeaders))
})
}
}

View File

@@ -34,6 +34,7 @@ type Provider struct {
Stale bool `description:"Use stale consistency for catalog reads" export:"true"`
ExposedByDefault bool `description:"Expose Consul services by default" export:"true"`
Prefix string `description:"Prefix used for Consul catalog tags" export:"true"`
StrictChecks bool `description:"Keep a Consul node only if all checks status are passing" export:"true"`
FrontEndRule string `description:"Frontend rule used for Consul services" export:"true"`
TLS *types.ClientTLS `description:"Enable TLS support" export:"true"`
client *api.Client
@@ -301,6 +302,8 @@ func (p *Provider) watchHealthState(stopCh <-chan struct{}, watchCh chan<- map[s
_, failing := currentFailing[key]
if healthy.Status == "passing" && !failing {
current[key] = append(current[key], healthy.Node)
} else if !p.StrictChecks && healthy.Status == "warning" && !failing {
current[key] = append(current[key], healthy.Node)
} else if strings.HasPrefix(healthy.CheckID, "_service_maintenance") || strings.HasPrefix(healthy.CheckID, "_node_maintenance") {
maintenance = append(maintenance, healthy.CheckID)
} else {
@@ -489,7 +492,8 @@ func getServiceAddresses(services []*api.CatalogService) []string {
func (p *Provider) healthyNodes(service string) (catalogUpdate, error) {
health := p.client.Health()
data, _, err := health.Service(service, "", true, &api.QueryOptions{AllowStale: p.Stale})
// You can't filter with assigning passingOnly here, nodeFilter will do this later
data, _, err := health.Service(service, "", false, &api.QueryOptions{AllowStale: p.Stale})
if err != nil {
log.WithError(err).Errorf("Failed to fetch details of %s", service)
return catalogUpdate{}, err
@@ -533,7 +537,8 @@ func (p *Provider) nodeFilter(service string, node *api.ServiceEntry) bool {
log.Debugf("Service %v pruned by '%v' constraint", service, failingConstraint.String())
return false
}
return true
return p.hasPassingChecks(node)
}
func (p *Provider) isServiceEnabled(node *api.ServiceEntry) bool {
@@ -567,6 +572,11 @@ func (p *Provider) getConstraintTags(tags []string) []string {
return values
}
func (p *Provider) hasPassingChecks(node *api.ServiceEntry) bool {
status := node.Checks.AggregatedStatus()
return status == "passing" || !p.StrictChecks && status == "warning"
}
func (p *Provider) generateFrontends(service *serviceUpdate) []*serviceUpdate {
frontends := make([]*serviceUpdate, 0)
// to support <prefix>.frontend.xxx

View File

@@ -28,7 +28,9 @@ import (
)
const (
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use
DockerAPIVersion = "1.24"
// SwarmAPIVersion is a constant holding the version of the Provider API traefik will use.
SwarmAPIVersion = "1.24"
)
@@ -110,11 +112,10 @@ func (p *Provider) createClient() (client.APIClient, error) {
"User-Agent": "Traefik " + version.Version,
}
var apiVersion string
apiVersion := DockerAPIVersion
if p.SwarmMode {
apiVersion = SwarmAPIVersion
} else {
apiVersion = DockerAPIVersion
}
return client.NewClient(p.Endpoint, apiVersion, httpClient, httpHeaders)

View File

@@ -1,8 +0,0 @@
// +build !windows
package docker
const (
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use
DockerAPIVersion = "1.21"
)

View File

@@ -1,6 +0,0 @@
package docker
const (
// DockerAPIVersion is a constant holding the version of the Provider API traefik will use
DockerAPIVersion string = "1.24"
)

View File

@@ -0,0 +1,13 @@
apiVersion: v1
kind: Endpoints
metadata:
name: service1
namespace: testing
subsets:
- addresses:
- ip: 10.10.0.1
ports:
- port: 8080
name: http
- port: 1111
name: foo

View File

@@ -0,0 +1,8 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
namespace: testing
spec:
backend:
serviceName: service1
servicePort: 80

View File

@@ -0,0 +1,14 @@
apiVersion: v1
kind: Service
metadata:
name: service1
namespace: testing
spec:
clusterIP: 10.0.0.1
ports:
- name: http
port: 80
targetPort: 8080
- name: foo
port: 1111
targetPort: 1111

View File

@@ -16,6 +16,7 @@ import (
"time"
"github.com/cenk/backoff"
"github.com/containous/flaeg"
"github.com/containous/traefik/job"
"github.com/containous/traefik/log"
"github.com/containous/traefik/provider"
@@ -68,6 +69,7 @@ type Provider struct {
LabelSelector string `description:"Kubernetes Ingress label selector to use" export:"true"`
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation to watch for" export:"true"`
IngressEndpoint *IngressEndpoint `description:"Kubernetes Ingress Endpoint"`
ThrottleDuration flaeg.Duration `description:"Ingress refresh throttle duration"`
lastConfiguration safe.Safe
}
@@ -137,16 +139,29 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
return nil
}
}
throttleDuration := time.Duration(p.ThrottleDuration)
throttledChan := throttleEvents(throttleDuration, stop, eventsChan)
if throttledChan != nil {
eventsChan = throttledChan
}
for {
select {
case <-stop:
return nil
case event := <-eventsChan:
// Note that event is the *first* event that came in during this
// throttling interval -- if we're hitting our throttle, we may have
// dropped events. This is fine, because we don't treat different
// event types differently. But if we do in the future, we'll need to
// track more information about the dropped events.
log.Debugf("Received Kubernetes event kind %T", event)
templateObjects, err := p.loadIngresses(k8sClient)
if err != nil {
return err
}
if reflect.DeepEqual(p.lastConfiguration.Get(), templateObjects) {
log.Debugf("Skipping Kubernetes event kind %T", event)
} else {
@@ -156,6 +171,11 @@ func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *s
Configuration: p.loadConfig(*templateObjects),
}
}
// If we're throttling, we sleep here for the throttle duration to
// enforce that we don't refresh faster than our throttle. time.Sleep
// returns immediately if p.ThrottleDuration is 0 (no throttle).
time.Sleep(throttleDuration)
}
}
}
@@ -508,6 +528,10 @@ func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, tem
for _, port := range service.Spec.Ports {
if !equalPorts(port, i.Spec.Backend.ServicePort) {
continue
}
// We have to treat external-name service differently here b/c it doesn't have any endpoints
if service.Spec.Type == corev1.ServiceTypeExternalName {
@@ -595,6 +619,39 @@ func (p *Provider) addGlobalBackend(cl Client, i *extensionsv1beta1.Ingress, tem
return nil
}
func throttleEvents(throttleDuration time.Duration, stop chan bool, eventsChan <-chan interface{}) chan interface{} {
if throttleDuration == 0 {
return nil
}
// Create a buffered channel to hold the pending event (if we're delaying processing the event due to throttling)
eventsChanBuffered := make(chan interface{}, 1)
// Run a goroutine that reads events from eventChan and does a
// non-blocking write to pendingEvent. This guarantees that writing to
// eventChan will never block, and that pendingEvent will have
// something in it if there's been an event since we read from that channel.
go func() {
for {
select {
case <-stop:
return
case nextEvent := <-eventsChan:
select {
case eventsChanBuffered <- nextEvent:
default:
// We already have an event in eventsChanBuffered, so we'll
// do a refresh as soon as our throttle allows us to. It's fine
// to drop the event and keep whatever's in the buffer -- we
// don't do different things for different events
log.Debugf("Dropping event kind %T due to throttling", nextEvent)
}
}
}
}()
return eventsChanBuffered
}
func getRuleForPath(pa extensionsv1beta1.HTTPIngressPath, i *extensionsv1beta1.Ingress) (string, error) {
if len(pa.Path) == 0 {
return "", nil
@@ -936,12 +993,12 @@ func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*type
}
authSecretName := getStringValue(i.Annotations, annotationKubernetesAuthForwardTLSSecret, "")
if len(authSecretName) > 0 {
authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient)
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %s", err)
}
authSecretCert, authSecretKey, err := loadAuthTLSSecret(i.Namespace, authSecretName, k8sClient)
if err != nil {
return nil, fmt.Errorf("failed to load auth secret: %s", err)
}
if authSecretCert != "" || authSecretKey != "" {
forwardAuth.TLS = &types.ClientTLS{
Cert: authSecretCert,
Key: authSecretKey,
@@ -949,10 +1006,20 @@ func getForwardAuthConfig(i *extensionsv1beta1.Ingress, k8sClient Client) (*type
}
}
if forwardAuth.TLS == nil && label.Has(i.Annotations, getAnnotationName(i.Annotations, annotationKubernetesAuthForwardTLSInsecure)) {
forwardAuth.TLS = &types.ClientTLS{
InsecureSkipVerify: getBoolValue(i.Annotations, annotationKubernetesAuthForwardTLSInsecure, false),
}
}
return forwardAuth, nil
}
func loadAuthTLSSecret(namespace, secretName string, k8sClient Client) (string, string, error) {
if len(secretName) == 0 {
return "", "", nil
}
secret, exists, err := k8sClient.GetSecret(namespace, secretName)
if err != nil {
return "", "", fmt.Errorf("failed to fetch secret %q/%q: %s", namespace, secretName, err)

View File

@@ -168,6 +168,33 @@ func TestProvider_loadIngresses(t *testing.T) {
),
),
},
{
desc: "loadGlobalIngressWithMultiplePortNumbers",
fixtures: []string{
filepath.Join("fixtures", "loadGlobalIngressWithMultiplePortNumbers_ingresses.yml"),
filepath.Join("fixtures", "loadGlobalIngressWithMultiplePortNumbers_services.yml"),
filepath.Join("fixtures", "loadGlobalIngressWithMultiplePortNumbers_endpoints.yml"),
},
expected: buildConfiguration(
backends(
backend("global-default-backend",
lbMethod("wrr"),
servers(
server("http://10.10.0.1:8080", weight(1)),
),
),
),
frontends(
frontend("global-default-backend",
frontendName("global-default-frontend"),
passHostHeader(),
routes(
route("/", "PathPrefix:/"),
),
),
),
),
},
{
desc: "loadGlobalIngressWithHttpsPortNames",
fixtures: []string{

View File

@@ -5,7 +5,7 @@ if [ -z "${VALIDATE_UPSTREAM:-}" ]; then
# are running more than one validate bundlescript
VALIDATE_REPO='https://github.com/containous/traefik.git'
VALIDATE_BRANCH='master'
VALIDATE_BRANCH='v1.7'
# Should not be needed for now O:)
# if [ "$TRAVIS" = 'true' -a "$TRAVIS_PULL_REQUEST" != 'false' ]; then

View File

@@ -3,9 +3,8 @@ set -e
# List of bundles to create when no argument is passed
DEFAULT_BUNDLES=(
validate-gofmt
validate-govet
generate
validate-gofmt
binary
test-unit

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
source "$(dirname "$BASH_SOURCE")/.validate"
IFS=$'\n'
files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) )
unset IFS
errors=()
failedErrcheck=$(errcheck .)
if [ "$failedErrcheck" ]; then
errors+=( "$failedErrcheck" )
fi
if [ ${#errors[@]} -eq 0 ]; then
echo 'Congratulations! All Go source files have been errchecked.'
else
{
echo "Errors from errcheck:"
for err in "${errors[@]}"; do
echo "$err"
done
echo
echo 'Please fix the above errors. You can test via "errcheck" and commit the result.'
echo
} >&2
false
fi

View File

@@ -1,31 +0,0 @@
#!/usr/bin/env bash
source "$(dirname "$BASH_SOURCE")/.validate"
IFS=$'\n'
files=( $(validate_diff --diff-filter=ACMR --name-only -- '*.go' | grep -v '^vendor/' || true) )
unset IFS
errors=()
for f in "${files[@]}"; do
# we use "git show" here to validate that what's committed passes go vet
failedVet=$(go tool vet -printf=false "$f")
if [ "$failedVet" ]; then
errors+=( "$failedVet" )
fi
done
if [ ${#errors[@]} -eq 0 ]; then
echo 'Congratulations! All Go source files have been vetted.'
else
{
echo "Errors from govet:"
for err in "${errors[@]}"; do
echo "$err"
done
echo
echo 'Please fix the above errors. You can test via "go vet" and commit the result.'
echo
} >&2
false
fi

View File

@@ -85,6 +85,8 @@ func (h *hijackConnectionTracker) Shutdown(ctx context.Context) error {
// Close close all the connections in the tracked connections list
func (h *hijackConnectionTracker) Close() {
h.lock.Lock()
defer h.lock.Unlock()
for conn := range h.conns {
if err := conn.Close(); err != nil {
log.Errorf("Error while closing Hijacked conn: %v", err)

View File

@@ -113,7 +113,7 @@ func (s *Server) buildMiddlewares(frontendName string, frontend *types.Frontend,
}
// TLSClientHeaders
tlsClientHeadersMiddleware := middlewares.NewTLSClientHeaders(frontend)
tlsClientHeadersMiddleware := middlewares.NewTLSClientHeaders(frontend.PassTLSClientCert)
if tlsClientHeadersMiddleware != nil {
log.Debugf("Adding TLSClientHeaders middleware for frontend %s", frontendName)

View File

@@ -59,7 +59,9 @@
{{if $frontend.Auth }}
[frontends."{{ $frontendName }}".auth]
headerField = "X-WebAuth-User"
{{if $frontend.Auth.HeaderField }}
headerField = "{{ $frontend.Auth.HeaderField }}"
{{end}}
{{if $frontend.Auth.Basic }}
[frontends."{{ $frontendName }}".auth.basic]

View File

@@ -16,36 +16,41 @@ import (
var (
// MinVersion Map of allowed TLS minimum versions
MinVersion = map[string]uint16{
`VersionTLS10`: tls.VersionTLS10,
`VersionTLS11`: tls.VersionTLS11,
`VersionTLS12`: tls.VersionTLS12,
"VersionTLS10": tls.VersionTLS10,
"VersionTLS11": tls.VersionTLS11,
"VersionTLS12": tls.VersionTLS12,
"VersionTLS13": tls.VersionTLS13,
}
// CipherSuites Map of TLS CipherSuites from crypto/tls
// Available CipherSuites defined at https://golang.org/pkg/crypto/tls/#pkg-constants
CipherSuites = map[string]uint16{
`TLS_RSA_WITH_RC4_128_SHA`: tls.TLS_RSA_WITH_RC4_128_SHA,
`TLS_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
`TLS_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_RSA_WITH_AES_128_CBC_SHA,
`TLS_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_RSA_WITH_AES_256_CBC_SHA,
`TLS_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
`TLS_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
`TLS_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
`TLS_ECDHE_ECDSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
`TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
`TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
`TLS_ECDHE_RSA_WITH_RC4_128_SHA`: tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
`TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
`TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
`TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA`: tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
`TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
`TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
`TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
`TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256`: tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
`TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
`TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384`: tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
`TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
`TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305`: tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
"TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA,
"TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"TLS_RSA_WITH_AES_256_CBC_SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"TLS_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
"TLS_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
"TLS_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_RSA_WITH_RC4_128_SHA": tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA,
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
"TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256,
"TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384,
"TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256,
"TLS_FALLBACK_SCSV": tls.TLS_FALLBACK_SCSV,
}
)

View File

@@ -2,6 +2,7 @@ package tls
import (
"crypto/tls"
"crypto/x509"
"net"
"sort"
"strings"
@@ -47,6 +48,11 @@ func (c CertificateStore) GetAllDomains() []string {
allCerts = append(allCerts, domains)
}
}
// Get Default certificate
if c.DefaultCertificate != nil {
allCerts = append(allCerts, getCertificateDomains(c.DefaultCertificate)...)
}
return allCerts
}
@@ -115,6 +121,27 @@ func (c CertificateStore) ResetCache() {
}
}
func getCertificateDomains(cert *tls.Certificate) []string {
if cert == nil {
return nil
}
x509Cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return nil
}
var names []string
if len(x509Cert.Subject.CommonName) > 0 {
names = append(names, x509Cert.Subject.CommonName)
}
for _, san := range x509Cert.DNSNames {
names = append(names, san)
}
return names
}
// MatchDomain return true if a domain match the cert domain
func MatchDomain(domain string, certDomain string) bool {
if domain == certDomain {

View File

@@ -13,6 +13,90 @@ import (
"github.com/stretchr/testify/require"
)
func TestGetAllDomains(t *testing.T) {
testCases := []struct {
desc string
staticCert string
dynamicCert string
defaultCert string
expectedDomains []string
}{
{
desc: "Empty Store, returns no domains",
staticCert: "",
dynamicCert: "",
defaultCert: "",
expectedDomains: nil,
},
{
desc: "Static cert domains",
staticCert: "snitest.com",
dynamicCert: "",
defaultCert: "",
expectedDomains: []string{"snitest.com"},
},
{
desc: "Dynamic cert domains",
staticCert: "",
dynamicCert: "snitest.com",
defaultCert: "",
expectedDomains: []string{"snitest.com"},
},
{
desc: "Default cert domains",
staticCert: "",
dynamicCert: "",
defaultCert: "snitest.com",
expectedDomains: []string{"snitest.com"},
},
{
desc: "All domains",
staticCert: "www.snitest.com",
dynamicCert: "*.snitest.com",
defaultCert: "snitest.com",
expectedDomains: []string{"www.snitest.com", "*.snitest.com", "snitest.com"},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
staticMap := map[string]*tls.Certificate{}
if test.staticCert != "" {
cert, err := loadTestCert(test.staticCert, false)
require.NoError(t, err)
staticMap[strings.ToLower(test.staticCert)] = cert
}
dynamicMap := map[string]*tls.Certificate{}
if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert, false)
require.NoError(t, err)
dynamicMap[strings.ToLower(test.dynamicCert)] = cert
}
var defaultCert *tls.Certificate
if test.defaultCert != "" {
cert, err := loadTestCert(test.defaultCert, false)
require.NoError(t, err)
defaultCert = cert
}
store := &CertificateStore{
DynamicCerts: safe.New(dynamicMap),
StaticCerts: safe.New(staticMap),
DefaultCertificate: defaultCert,
CertCache: cache.New(1*time.Hour, 10*time.Minute),
}
actual := store.GetAllDomains()
assert.Equal(t, test.expectedDomains, actual)
})
}
}
func TestGetBestCertificate(t *testing.T) {
testCases := []struct {
desc string
@@ -116,15 +200,15 @@ func TestGetBestCertificate(t *testing.T) {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
staticMap := map[string]*tls.Certificate{}
dynamicMap := map[string]*tls.Certificate{}
staticMap := map[string]*tls.Certificate{}
if test.staticCert != "" {
cert, err := loadTestCert(test.staticCert, test.uppercase)
require.NoError(t, err)
staticMap[strings.ToLower(test.staticCert)] = cert
}
dynamicMap := map[string]*tls.Certificate{}
if test.dynamicCert != "" {
cert, err := loadTestCert(test.dynamicCert, test.uppercase)
require.NoError(t, err)

View File

@@ -85,7 +85,7 @@ func derCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]by
NotBefore: time.Now(),
NotAfter: expiration,
KeyUsage: x509.KeyUsageKeyEncipherment,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageKeyAgreement | x509.KeyUsageDataEncipherment,
BasicConstraintsValid: true,
DNSNames: []string{domain},
}

View File

@@ -408,14 +408,14 @@ type Users []string
// Basic HTTP basic authentication
type Basic struct {
Users `json:"-" mapstructure:","`
Users `json:"-" mapstructure:"," dynamodbav:"users,omitempty"`
UsersFile string `json:"usersFile,omitempty"`
RemoveHeader bool `json:"removeHeader,omitempty"`
}
// Digest HTTP authentication
type Digest struct {
Users `json:"-" mapstructure:","`
Users `json:"-" mapstructure:"," dynamodbav:"users,omitempty"`
UsersFile string `json:"usersFile,omitempty"`
RemoveHeader bool `json:"removeHeader,omitempty"`
}
@@ -511,7 +511,7 @@ type ClientTLS struct {
CA string `description:"TLS CA" json:"ca,omitempty"`
CAOptional bool `description:"TLS CA.Optional" json:"caOptional,omitempty"`
Cert string `description:"TLS cert" json:"cert,omitempty"`
Key string `description:"TLS key" json:"-"`
Key string `description:"TLS key" json:"-" dynamodbav:"key,omitempty"`
InsecureSkipVerify bool `description:"TLS insecure skip verify" json:"insecureSkipVerify,omitempty"`
}

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.

View File

@@ -0,0 +1,38 @@
// Copyright 2018, OpenCensus 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 ocagent
import (
"math/rand"
"time"
)
var randSrc = rand.New(rand.NewSource(time.Now().UnixNano()))
// retries function fn upto n times, if fn returns an error lest it returns nil early.
// It applies exponential backoff in units of (1<<n) + jitter microsends.
func nTriesWithExponentialBackoff(nTries int64, timeBaseUnit time.Duration, fn func() error) (err error) {
for i := int64(0); i < nTries; i++ {
err = fn()
if err == nil {
return nil
}
// Backoff for a time period with a pseudo-random jitter
jitter := time.Duration(randSrc.Float64()*100) * time.Microsecond
ts := jitter + ((1 << uint64(i)) * timeBaseUnit)
<-time.After(ts)
}
return err
}

View File

@@ -0,0 +1,97 @@
// Copyright 2018, OpenCensus 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 ocagent
import (
"math/rand"
"sync/atomic"
"time"
)
const (
sDisconnected int32 = 5 + iota
sConnected
)
func (ae *Exporter) setStateDisconnected() {
atomic.StoreInt32(&ae.connectionState, sDisconnected)
select {
case ae.disconnectedCh <- true:
default:
}
}
func (ae *Exporter) setStateConnected() {
atomic.StoreInt32(&ae.connectionState, sConnected)
}
func (ae *Exporter) connected() bool {
return atomic.LoadInt32(&ae.connectionState) == sConnected
}
const defaultConnReattemptPeriod = 10 * time.Second
func (ae *Exporter) indefiniteBackgroundConnection() error {
defer func() {
ae.backgroundConnectionDoneCh <- true
}()
connReattemptPeriod := ae.reconnectionPeriod
if connReattemptPeriod <= 0 {
connReattemptPeriod = defaultConnReattemptPeriod
}
// No strong seeding required, nano time can
// already help with pseudo uniqueness.
rng := rand.New(rand.NewSource(time.Now().UnixNano() + rand.Int63n(1024)))
// maxJitter: 1 + (70% of the connectionReattemptPeriod)
maxJitter := int64(1 + 0.7*float64(connReattemptPeriod))
for {
// Otherwise these will be the normal scenarios to enable
// reconnections if we trip out.
// 1. If we've stopped, return entirely
// 2. Otherwise block until we are disconnected, and
// then retry connecting
select {
case <-ae.stopCh:
return errStopped
case <-ae.disconnectedCh:
// Normal scenario that we'll wait for
}
if err := ae.connect(); err == nil {
ae.setStateConnected()
} else {
ae.setStateDisconnected()
}
// Apply some jitter to avoid lockstep retrials of other
// agent-exporters. Lockstep retrials could result in an
// innocent DDOS, by clogging the machine's resources and network.
jitter := time.Duration(rng.Int63n(maxJitter))
<-time.After(connReattemptPeriod + jitter)
}
}
func (ae *Exporter) connect() error {
cc, err := ae.dialToAgent()
if err != nil {
return err
}
return ae.enableConnectionStreams(cc)
}

View File

@@ -0,0 +1,46 @@
// Copyright 2018, OpenCensus 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 ocagent
import (
"os"
commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
"go.opencensus.io"
)
// NodeWithStartTime creates a node using nodeName and derives:
// Hostname from the environment
// Pid from the current process
// StartTimestamp from the start time of this process
// Language and library information.
func NodeWithStartTime(nodeName string) *commonpb.Node {
return &commonpb.Node{
Identifier: &commonpb.ProcessIdentifier{
HostName: os.Getenv("HOSTNAME"),
Pid: uint32(os.Getpid()),
StartTimestamp: timeToTimestamp(startTime),
},
LibraryInfo: &commonpb.LibraryInfo{
Language: commonpb.LibraryInfo_GO_LANG,
ExporterVersion: Version,
CoreLibraryVersion: opencensus.Version(),
},
ServiceInfo: &commonpb.ServiceInfo{
Name: nodeName,
},
Attributes: make(map[string]string),
}
}

View File

@@ -0,0 +1,496 @@
// Copyright 2018, OpenCensus 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 ocagent
import (
"context"
"errors"
"fmt"
"sync"
"time"
"google.golang.org/api/support/bundler"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"go.opencensus.io/plugin/ocgrpc"
"go.opencensus.io/resource"
"go.opencensus.io/stats/view"
"go.opencensus.io/trace"
commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
agentmetricspb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/metrics/v1"
agenttracepb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/trace/v1"
metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"
tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
)
var startupMu sync.Mutex
var startTime time.Time
func init() {
startupMu.Lock()
startTime = time.Now()
startupMu.Unlock()
}
var _ trace.Exporter = (*Exporter)(nil)
var _ view.Exporter = (*Exporter)(nil)
type Exporter struct {
connectionState int32
// mu protects the non-atomic and non-channel variables
mu sync.RWMutex
// senderMu protects the concurrent unsafe traceExporter client
senderMu sync.RWMutex
started bool
stopped bool
agentAddress string
serviceName string
canDialInsecure bool
traceExporter agenttracepb.TraceService_ExportClient
metricsExporter agentmetricspb.MetricsService_ExportClient
nodeInfo *commonpb.Node
grpcClientConn *grpc.ClientConn
reconnectionPeriod time.Duration
resource *resourcepb.Resource
compressor string
headers map[string]string
startOnce sync.Once
stopCh chan bool
disconnectedCh chan bool
backgroundConnectionDoneCh chan bool
traceBundler *bundler.Bundler
// viewDataBundler is the bundler to enable conversion
// from OpenCensus-Go view.Data to metricspb.Metric.
// Please do not confuse it with metricsBundler!
viewDataBundler *bundler.Bundler
clientTransportCredentials credentials.TransportCredentials
}
func NewExporter(opts ...ExporterOption) (*Exporter, error) {
exp, err := NewUnstartedExporter(opts...)
if err != nil {
return nil, err
}
if err := exp.Start(); err != nil {
return nil, err
}
return exp, nil
}
const spanDataBufferSize = 300
func NewUnstartedExporter(opts ...ExporterOption) (*Exporter, error) {
e := new(Exporter)
for _, opt := range opts {
opt.withExporter(e)
}
traceBundler := bundler.NewBundler((*trace.SpanData)(nil), func(bundle interface{}) {
e.uploadTraces(bundle.([]*trace.SpanData))
})
traceBundler.DelayThreshold = 2 * time.Second
traceBundler.BundleCountThreshold = spanDataBufferSize
e.traceBundler = traceBundler
viewDataBundler := bundler.NewBundler((*view.Data)(nil), func(bundle interface{}) {
e.uploadViewData(bundle.([]*view.Data))
})
viewDataBundler.DelayThreshold = 2 * time.Second
viewDataBundler.BundleCountThreshold = 500 // TODO: (@odeke-em) make this configurable.
e.viewDataBundler = viewDataBundler
e.nodeInfo = NodeWithStartTime(e.serviceName)
e.resource = resourceProtoFromEnv()
return e, nil
}
const (
maxInitialConfigRetries = 10
maxInitialTracesRetries = 10
)
var (
errAlreadyStarted = errors.New("already started")
errNotStarted = errors.New("not started")
errStopped = errors.New("stopped")
errNoConnection = errors.New("no active connection")
)
// Start dials to the agent, establishing a connection to it. It also
// initiates the Config and Trace services by sending over the initial
// messages that consist of the node identifier. Start invokes a background
// connector that will reattempt connections to the agent periodically
// if the connection dies.
func (ae *Exporter) Start() error {
var err = errAlreadyStarted
ae.startOnce.Do(func() {
ae.mu.Lock()
defer ae.mu.Unlock()
ae.started = true
ae.disconnectedCh = make(chan bool, 1)
ae.stopCh = make(chan bool)
ae.backgroundConnectionDoneCh = make(chan bool)
ae.setStateDisconnected()
go ae.indefiniteBackgroundConnection()
err = nil
})
return err
}
func (ae *Exporter) prepareAgentAddress() string {
if ae.agentAddress != "" {
return ae.agentAddress
}
return fmt.Sprintf("%s:%d", DefaultAgentHost, DefaultAgentPort)
}
func (ae *Exporter) enableConnectionStreams(cc *grpc.ClientConn) error {
ae.mu.RLock()
started := ae.started
nodeInfo := ae.nodeInfo
ae.mu.RUnlock()
if !started {
return errNotStarted
}
ae.mu.Lock()
// If the previous clientConn was non-nil, close it
if ae.grpcClientConn != nil {
_ = ae.grpcClientConn.Close()
}
ae.grpcClientConn = cc
ae.mu.Unlock()
if err := ae.createTraceServiceConnection(ae.grpcClientConn, nodeInfo); err != nil {
return err
}
return ae.createMetricsServiceConnection(ae.grpcClientConn, nodeInfo)
}
func (ae *Exporter) createTraceServiceConnection(cc *grpc.ClientConn, node *commonpb.Node) error {
// Initiate the trace service by sending over node identifier info.
traceSvcClient := agenttracepb.NewTraceServiceClient(cc)
ctx := context.Background()
if len(ae.headers) > 0 {
ctx = metadata.NewOutgoingContext(ctx, metadata.New(ae.headers))
}
traceExporter, err := traceSvcClient.Export(ctx)
if err != nil {
return fmt.Errorf("Exporter.Start:: TraceServiceClient: %v", err)
}
firstTraceMessage := &agenttracepb.ExportTraceServiceRequest{
Node: node,
Resource: ae.resource,
}
if err := traceExporter.Send(firstTraceMessage); err != nil {
return fmt.Errorf("Exporter.Start:: Failed to initiate the Config service: %v", err)
}
ae.mu.Lock()
ae.traceExporter = traceExporter
ae.mu.Unlock()
// Initiate the config service by sending over node identifier info.
configStream, err := traceSvcClient.Config(context.Background())
if err != nil {
return fmt.Errorf("Exporter.Start:: ConfigStream: %v", err)
}
firstCfgMessage := &agenttracepb.CurrentLibraryConfig{Node: node}
if err := configStream.Send(firstCfgMessage); err != nil {
return fmt.Errorf("Exporter.Start:: Failed to initiate the Config service: %v", err)
}
// In the background, handle trace configurations that are beamed down
// by the agent, but also reply to it with the applied configuration.
go ae.handleConfigStreaming(configStream)
return nil
}
func (ae *Exporter) createMetricsServiceConnection(cc *grpc.ClientConn, node *commonpb.Node) error {
metricsSvcClient := agentmetricspb.NewMetricsServiceClient(cc)
metricsExporter, err := metricsSvcClient.Export(context.Background())
if err != nil {
return fmt.Errorf("MetricsExporter: failed to start the service client: %v", err)
}
// Initiate the metrics service by sending over the first message just containing the Node and Resource.
firstMetricsMessage := &agentmetricspb.ExportMetricsServiceRequest{
Node: node,
Resource: ae.resource,
}
if err := metricsExporter.Send(firstMetricsMessage); err != nil {
return fmt.Errorf("MetricsExporter:: failed to send the first message: %v", err)
}
ae.mu.Lock()
ae.metricsExporter = metricsExporter
ae.mu.Unlock()
// With that we are good to go and can start sending metrics
return nil
}
func (ae *Exporter) dialToAgent() (*grpc.ClientConn, error) {
addr := ae.prepareAgentAddress()
var dialOpts []grpc.DialOption
if ae.clientTransportCredentials != nil {
dialOpts = append(dialOpts, grpc.WithTransportCredentials(ae.clientTransportCredentials))
} else if ae.canDialInsecure {
dialOpts = append(dialOpts, grpc.WithInsecure())
}
if ae.compressor != "" {
dialOpts = append(dialOpts, grpc.WithDefaultCallOptions(grpc.UseCompressor(ae.compressor)))
}
dialOpts = append(dialOpts, grpc.WithStatsHandler(&ocgrpc.ClientHandler{}))
ctx := context.Background()
if len(ae.headers) > 0 {
ctx = metadata.NewOutgoingContext(ctx, metadata.New(ae.headers))
}
return grpc.DialContext(ctx, addr, dialOpts...)
}
func (ae *Exporter) handleConfigStreaming(configStream agenttracepb.TraceService_ConfigClient) error {
// Note: We haven't yet implemented configuration sending so we
// should NOT be changing connection states within this function for now.
for {
recv, err := configStream.Recv()
if err != nil {
// TODO: Check if this is a transient error or exponential backoff-able.
return err
}
cfg := recv.Config
if cfg == nil {
continue
}
// Otherwise now apply the trace configuration sent down from the agent
if psamp := cfg.GetProbabilitySampler(); psamp != nil {
trace.ApplyConfig(trace.Config{DefaultSampler: trace.ProbabilitySampler(psamp.SamplingProbability)})
} else if csamp := cfg.GetConstantSampler(); csamp != nil {
alwaysSample := csamp.Decision == tracepb.ConstantSampler_ALWAYS_ON
if alwaysSample {
trace.ApplyConfig(trace.Config{DefaultSampler: trace.AlwaysSample()})
} else {
trace.ApplyConfig(trace.Config{DefaultSampler: trace.NeverSample()})
}
} else { // TODO: Add the rate limiting sampler here
}
// Then finally send back to upstream the newly applied configuration
err = configStream.Send(&agenttracepb.CurrentLibraryConfig{Config: &tracepb.TraceConfig{Sampler: cfg.Sampler}})
if err != nil {
return err
}
}
}
// Stop shuts down all the connections and resources
// related to the exporter.
func (ae *Exporter) Stop() error {
ae.mu.RLock()
cc := ae.grpcClientConn
started := ae.started
stopped := ae.stopped
ae.mu.RUnlock()
if !started {
return errNotStarted
}
if stopped {
// TODO: tell the user that we've already stopped, so perhaps a sentinel error?
return nil
}
ae.Flush()
// Now close the underlying gRPC connection.
var err error
if cc != nil {
err = cc.Close()
}
// At this point we can change the state variables: started and stopped
ae.mu.Lock()
ae.started = false
ae.stopped = true
ae.mu.Unlock()
close(ae.stopCh)
// Ensure that the backgroundConnector returns
<-ae.backgroundConnectionDoneCh
return err
}
func (ae *Exporter) ExportSpan(sd *trace.SpanData) {
if sd == nil {
return
}
_ = ae.traceBundler.Add(sd, 1)
}
func (ae *Exporter) ExportTraceServiceRequest(batch *agenttracepb.ExportTraceServiceRequest) error {
if batch == nil || len(batch.Spans) == 0 {
return nil
}
select {
case <-ae.stopCh:
return errStopped
default:
if !ae.connected() {
return errNoConnection
}
ae.senderMu.Lock()
err := ae.traceExporter.Send(batch)
ae.senderMu.Unlock()
if err != nil {
ae.setStateDisconnected()
return err
}
return nil
}
}
func (ae *Exporter) ExportView(vd *view.Data) {
if vd == nil {
return
}
_ = ae.viewDataBundler.Add(vd, 1)
}
func ocSpanDataToPbSpans(sdl []*trace.SpanData) []*tracepb.Span {
if len(sdl) == 0 {
return nil
}
protoSpans := make([]*tracepb.Span, 0, len(sdl))
for _, sd := range sdl {
if sd != nil {
protoSpans = append(protoSpans, ocSpanToProtoSpan(sd))
}
}
return protoSpans
}
func (ae *Exporter) uploadTraces(sdl []*trace.SpanData) {
select {
case <-ae.stopCh:
return
default:
if !ae.connected() {
return
}
protoSpans := ocSpanDataToPbSpans(sdl)
if len(protoSpans) == 0 {
return
}
ae.senderMu.Lock()
err := ae.traceExporter.Send(&agenttracepb.ExportTraceServiceRequest{
Spans: protoSpans,
})
ae.senderMu.Unlock()
if err != nil {
ae.setStateDisconnected()
}
}
}
func ocViewDataToPbMetrics(vdl []*view.Data) []*metricspb.Metric {
if len(vdl) == 0 {
return nil
}
metrics := make([]*metricspb.Metric, 0, len(vdl))
for _, vd := range vdl {
if vd != nil {
vmetric, err := viewDataToMetric(vd)
// TODO: (@odeke-em) somehow report this error, if it is non-nil.
if err == nil && vmetric != nil {
metrics = append(metrics, vmetric)
}
}
}
return metrics
}
func (ae *Exporter) uploadViewData(vdl []*view.Data) {
select {
case <-ae.stopCh:
return
default:
if !ae.connected() {
return
}
protoMetrics := ocViewDataToPbMetrics(vdl)
if len(protoMetrics) == 0 {
return
}
err := ae.metricsExporter.Send(&agentmetricspb.ExportMetricsServiceRequest{
Metrics: protoMetrics,
// TODO:(@odeke-em)
// a) Figure out how to derive a Node from the environment
// b) Figure out how to derive a Resource from the environment
// or better letting users of the exporter configure it.
})
if err != nil {
ae.setStateDisconnected()
}
}
}
func (ae *Exporter) Flush() {
ae.traceBundler.Flush()
ae.viewDataBundler.Flush()
}
func resourceProtoFromEnv() *resourcepb.Resource {
rs, _ := resource.FromEnv(context.Background())
if rs == nil {
return nil
}
rprs := &resourcepb.Resource{
Type: rs.Type,
}
if rs.Labels != nil {
rprs.Labels = make(map[string]string)
for k, v := range rs.Labels {
rprs.Labels[k] = v
}
}
return rprs
}

View File

@@ -0,0 +1,128 @@
// Copyright 2018, OpenCensus 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 ocagent
import (
"time"
"google.golang.org/grpc/credentials"
)
const (
DefaultAgentPort uint16 = 55678
DefaultAgentHost string = "localhost"
)
type ExporterOption interface {
withExporter(e *Exporter)
}
type insecureGrpcConnection int
var _ ExporterOption = (*insecureGrpcConnection)(nil)
func (igc *insecureGrpcConnection) withExporter(e *Exporter) {
e.canDialInsecure = true
}
// WithInsecure disables client transport security for the exporter's gRPC connection
// just like grpc.WithInsecure() https://godoc.org/google.golang.org/grpc#WithInsecure
// does. Note, by default, client security is required unless WithInsecure is used.
func WithInsecure() ExporterOption { return new(insecureGrpcConnection) }
type addressSetter string
func (as addressSetter) withExporter(e *Exporter) {
e.agentAddress = string(as)
}
var _ ExporterOption = (*addressSetter)(nil)
// WithAddress allows one to set the address that the exporter will
// connect to the agent on. If unset, it will instead try to use
// connect to DefaultAgentHost:DefaultAgentPort
func WithAddress(addr string) ExporterOption {
return addressSetter(addr)
}
type serviceNameSetter string
func (sns serviceNameSetter) withExporter(e *Exporter) {
e.serviceName = string(sns)
}
var _ ExporterOption = (*serviceNameSetter)(nil)
// WithServiceName allows one to set/override the service name
// that the exporter will report to the agent.
func WithServiceName(serviceName string) ExporterOption {
return serviceNameSetter(serviceName)
}
type reconnectionPeriod time.Duration
func (rp reconnectionPeriod) withExporter(e *Exporter) {
e.reconnectionPeriod = time.Duration(rp)
}
func WithReconnectionPeriod(rp time.Duration) ExporterOption {
return reconnectionPeriod(rp)
}
type compressorSetter string
func (c compressorSetter) withExporter(e *Exporter) {
e.compressor = string(c)
}
// UseCompressor will set the compressor for the gRPC client to use when sending requests.
// It is the responsibility of the caller to ensure that the compressor set has been registered
// with google.golang.org/grpc/encoding. This can be done by encoding.RegisterCompressor. Some
// compressors auto-register on import, such as gzip, which can be registered by calling
// `import _ "google.golang.org/grpc/encoding/gzip"`
func UseCompressor(compressorName string) ExporterOption {
return compressorSetter(compressorName)
}
type headerSetter map[string]string
func (h headerSetter) withExporter(e *Exporter) {
e.headers = map[string]string(h)
}
// WithHeaders will send the provided headers when the gRPC stream connection
// is instantiated
func WithHeaders(headers map[string]string) ExporterOption {
return headerSetter(headers)
}
type clientCredentials struct {
credentials.TransportCredentials
}
var _ ExporterOption = (*clientCredentials)(nil)
// WithTLSCredentials allows the connection to use TLS credentials
// when talking to the server. It takes in grpc.TransportCredentials instead
// of say a Certificate file or a tls.Certificate, because the retrieving
// these credentials can be done in many ways e.g. plain file, in code tls.Config
// or by certificate rotation, so it is up to the caller to decide what to use.
func WithTLSCredentials(creds credentials.TransportCredentials) ExporterOption {
return &clientCredentials{TransportCredentials: creds}
}
func (cc *clientCredentials) withExporter(e *Exporter) {
e.clientTransportCredentials = cc.TransportCredentials
}

View File

@@ -0,0 +1,248 @@
// Copyright 2018, OpenCensus 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 ocagent
import (
"math"
"time"
"go.opencensus.io/trace"
"go.opencensus.io/trace/tracestate"
tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
"github.com/golang/protobuf/ptypes/timestamp"
)
const (
maxAnnotationEventsPerSpan = 32
maxMessageEventsPerSpan = 128
)
func ocSpanToProtoSpan(sd *trace.SpanData) *tracepb.Span {
if sd == nil {
return nil
}
var namePtr *tracepb.TruncatableString
if sd.Name != "" {
namePtr = &tracepb.TruncatableString{Value: sd.Name}
}
return &tracepb.Span{
TraceId: sd.TraceID[:],
SpanId: sd.SpanID[:],
ParentSpanId: sd.ParentSpanID[:],
Status: ocStatusToProtoStatus(sd.Status),
StartTime: timeToTimestamp(sd.StartTime),
EndTime: timeToTimestamp(sd.EndTime),
Links: ocLinksToProtoLinks(sd.Links),
Kind: ocSpanKindToProtoSpanKind(sd.SpanKind),
Name: namePtr,
Attributes: ocAttributesToProtoAttributes(sd.Attributes),
TimeEvents: ocTimeEventsToProtoTimeEvents(sd.Annotations, sd.MessageEvents),
Tracestate: ocTracestateToProtoTracestate(sd.Tracestate),
}
}
var blankStatus trace.Status
func ocStatusToProtoStatus(status trace.Status) *tracepb.Status {
if status == blankStatus {
return nil
}
return &tracepb.Status{
Code: status.Code,
Message: status.Message,
}
}
func ocLinksToProtoLinks(links []trace.Link) *tracepb.Span_Links {
if len(links) == 0 {
return nil
}
sl := make([]*tracepb.Span_Link, 0, len(links))
for _, ocLink := range links {
// This redefinition is necessary to prevent ocLink.*ID[:] copies
// being reused -- in short we need a new ocLink per iteration.
ocLink := ocLink
sl = append(sl, &tracepb.Span_Link{
TraceId: ocLink.TraceID[:],
SpanId: ocLink.SpanID[:],
Type: ocLinkTypeToProtoLinkType(ocLink.Type),
})
}
return &tracepb.Span_Links{
Link: sl,
}
}
func ocLinkTypeToProtoLinkType(oct trace.LinkType) tracepb.Span_Link_Type {
switch oct {
case trace.LinkTypeChild:
return tracepb.Span_Link_CHILD_LINKED_SPAN
case trace.LinkTypeParent:
return tracepb.Span_Link_PARENT_LINKED_SPAN
default:
return tracepb.Span_Link_TYPE_UNSPECIFIED
}
}
func ocAttributesToProtoAttributes(attrs map[string]interface{}) *tracepb.Span_Attributes {
if len(attrs) == 0 {
return nil
}
outMap := make(map[string]*tracepb.AttributeValue)
for k, v := range attrs {
switch v := v.(type) {
case bool:
outMap[k] = &tracepb.AttributeValue{Value: &tracepb.AttributeValue_BoolValue{BoolValue: v}}
case int:
outMap[k] = &tracepb.AttributeValue{Value: &tracepb.AttributeValue_IntValue{IntValue: int64(v)}}
case int64:
outMap[k] = &tracepb.AttributeValue{Value: &tracepb.AttributeValue_IntValue{IntValue: v}}
case string:
outMap[k] = &tracepb.AttributeValue{
Value: &tracepb.AttributeValue_StringValue{
StringValue: &tracepb.TruncatableString{Value: v},
},
}
}
}
return &tracepb.Span_Attributes{
AttributeMap: outMap,
}
}
// This code is mostly copied from
// https://github.com/census-ecosystem/opencensus-go-exporter-stackdriver/blob/master/trace_proto.go#L46
func ocTimeEventsToProtoTimeEvents(as []trace.Annotation, es []trace.MessageEvent) *tracepb.Span_TimeEvents {
if len(as) == 0 && len(es) == 0 {
return nil
}
timeEvents := &tracepb.Span_TimeEvents{}
var annotations, droppedAnnotationsCount int
var messageEvents, droppedMessageEventsCount int
// Transform annotations
for i, a := range as {
if annotations >= maxAnnotationEventsPerSpan {
droppedAnnotationsCount = len(as) - i
break
}
annotations++
timeEvents.TimeEvent = append(timeEvents.TimeEvent,
&tracepb.Span_TimeEvent{
Time: timeToTimestamp(a.Time),
Value: transformAnnotationToTimeEvent(&a),
},
)
}
// Transform message events
for i, e := range es {
if messageEvents >= maxMessageEventsPerSpan {
droppedMessageEventsCount = len(es) - i
break
}
messageEvents++
timeEvents.TimeEvent = append(timeEvents.TimeEvent,
&tracepb.Span_TimeEvent{
Time: timeToTimestamp(e.Time),
Value: transformMessageEventToTimeEvent(&e),
},
)
}
// Process dropped counter
timeEvents.DroppedAnnotationsCount = clip32(droppedAnnotationsCount)
timeEvents.DroppedMessageEventsCount = clip32(droppedMessageEventsCount)
return timeEvents
}
func transformAnnotationToTimeEvent(a *trace.Annotation) *tracepb.Span_TimeEvent_Annotation_ {
return &tracepb.Span_TimeEvent_Annotation_{
Annotation: &tracepb.Span_TimeEvent_Annotation{
Description: &tracepb.TruncatableString{Value: a.Message},
Attributes: ocAttributesToProtoAttributes(a.Attributes),
},
}
}
func transformMessageEventToTimeEvent(e *trace.MessageEvent) *tracepb.Span_TimeEvent_MessageEvent_ {
return &tracepb.Span_TimeEvent_MessageEvent_{
MessageEvent: &tracepb.Span_TimeEvent_MessageEvent{
Type: tracepb.Span_TimeEvent_MessageEvent_Type(e.EventType),
Id: uint64(e.MessageID),
UncompressedSize: uint64(e.UncompressedByteSize),
CompressedSize: uint64(e.CompressedByteSize),
},
}
}
// clip32 clips an int to the range of an int32.
func clip32(x int) int32 {
if x < math.MinInt32 {
return math.MinInt32
}
if x > math.MaxInt32 {
return math.MaxInt32
}
return int32(x)
}
func timeToTimestamp(t time.Time) *timestamp.Timestamp {
nanoTime := t.UnixNano()
return &timestamp.Timestamp{
Seconds: nanoTime / 1e9,
Nanos: int32(nanoTime % 1e9),
}
}
func ocSpanKindToProtoSpanKind(kind int) tracepb.Span_SpanKind {
switch kind {
case trace.SpanKindClient:
return tracepb.Span_CLIENT
case trace.SpanKindServer:
return tracepb.Span_SERVER
default:
return tracepb.Span_SPAN_KIND_UNSPECIFIED
}
}
func ocTracestateToProtoTracestate(ts *tracestate.Tracestate) *tracepb.Span_Tracestate {
if ts == nil {
return nil
}
return &tracepb.Span_Tracestate{
Entries: ocTracestateEntriesToProtoTracestateEntries(ts.Entries()),
}
}
func ocTracestateEntriesToProtoTracestateEntries(entries []tracestate.Entry) []*tracepb.Span_Tracestate_Entry {
protoEntries := make([]*tracepb.Span_Tracestate_Entry, 0, len(entries))
for _, entry := range entries {
protoEntries = append(protoEntries, &tracepb.Span_Tracestate_Entry{
Key: entry.Key,
Value: entry.Value,
})
}
return protoEntries
}

View File

@@ -0,0 +1,274 @@
// Copyright 2018, OpenCensus 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 ocagent
import (
"errors"
"time"
"go.opencensus.io/stats"
"go.opencensus.io/stats/view"
"go.opencensus.io/tag"
"github.com/golang/protobuf/ptypes/timestamp"
metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
)
var (
errNilMeasure = errors.New("expecting a non-nil stats.Measure")
errNilView = errors.New("expecting a non-nil view.View")
errNilViewData = errors.New("expecting a non-nil view.Data")
)
func viewDataToMetric(vd *view.Data) (*metricspb.Metric, error) {
if vd == nil {
return nil, errNilViewData
}
descriptor, err := viewToMetricDescriptor(vd.View)
if err != nil {
return nil, err
}
timeseries, err := viewDataToTimeseries(vd)
if err != nil {
return nil, err
}
metric := &metricspb.Metric{
MetricDescriptor: descriptor,
Timeseries: timeseries,
}
return metric, nil
}
func viewToMetricDescriptor(v *view.View) (*metricspb.MetricDescriptor, error) {
if v == nil {
return nil, errNilView
}
if v.Measure == nil {
return nil, errNilMeasure
}
desc := &metricspb.MetricDescriptor{
Name: stringOrCall(v.Name, v.Measure.Name),
Description: stringOrCall(v.Description, v.Measure.Description),
Unit: v.Measure.Unit(),
Type: aggregationToMetricDescriptorType(v),
LabelKeys: tagKeysToLabelKeys(v.TagKeys),
}
return desc, nil
}
func stringOrCall(first string, call func() string) string {
if first != "" {
return first
}
return call()
}
type measureType uint
const (
measureUnknown measureType = iota
measureInt64
measureFloat64
)
func measureTypeFromMeasure(m stats.Measure) measureType {
switch m.(type) {
default:
return measureUnknown
case *stats.Float64Measure:
return measureFloat64
case *stats.Int64Measure:
return measureInt64
}
}
func aggregationToMetricDescriptorType(v *view.View) metricspb.MetricDescriptor_Type {
if v == nil || v.Aggregation == nil {
return metricspb.MetricDescriptor_UNSPECIFIED
}
if v.Measure == nil {
return metricspb.MetricDescriptor_UNSPECIFIED
}
switch v.Aggregation.Type {
case view.AggTypeCount:
// Cumulative on int64
return metricspb.MetricDescriptor_CUMULATIVE_INT64
case view.AggTypeDistribution:
// Cumulative types
return metricspb.MetricDescriptor_CUMULATIVE_DISTRIBUTION
case view.AggTypeLastValue:
// Gauge types
switch measureTypeFromMeasure(v.Measure) {
case measureFloat64:
return metricspb.MetricDescriptor_GAUGE_DOUBLE
case measureInt64:
return metricspb.MetricDescriptor_GAUGE_INT64
}
case view.AggTypeSum:
// Cumulative types
switch measureTypeFromMeasure(v.Measure) {
case measureFloat64:
return metricspb.MetricDescriptor_CUMULATIVE_DOUBLE
case measureInt64:
return metricspb.MetricDescriptor_CUMULATIVE_INT64
}
}
// For all other cases, return unspecified.
return metricspb.MetricDescriptor_UNSPECIFIED
}
func tagKeysToLabelKeys(tagKeys []tag.Key) []*metricspb.LabelKey {
labelKeys := make([]*metricspb.LabelKey, 0, len(tagKeys))
for _, tagKey := range tagKeys {
labelKeys = append(labelKeys, &metricspb.LabelKey{
Key: tagKey.Name(),
})
}
return labelKeys
}
func viewDataToTimeseries(vd *view.Data) ([]*metricspb.TimeSeries, error) {
if vd == nil || len(vd.Rows) == 0 {
return nil, nil
}
// Given that view.Data only contains Start, End
// the timestamps for all the row data will be the exact same
// per aggregation. However, the values will differ.
// Each row has its own tags.
startTimestamp := timeToProtoTimestamp(vd.Start)
endTimestamp := timeToProtoTimestamp(vd.End)
mType := measureTypeFromMeasure(vd.View.Measure)
timeseries := make([]*metricspb.TimeSeries, 0, len(vd.Rows))
// It is imperative that the ordering of "LabelValues" matches those
// of the Label keys in the metric descriptor.
for _, row := range vd.Rows {
labelValues := labelValuesFromTags(row.Tags)
point := rowToPoint(vd.View, row, endTimestamp, mType)
timeseries = append(timeseries, &metricspb.TimeSeries{
StartTimestamp: startTimestamp,
LabelValues: labelValues,
Points: []*metricspb.Point{point},
})
}
if len(timeseries) == 0 {
return nil, nil
}
return timeseries, nil
}
func timeToProtoTimestamp(t time.Time) *timestamp.Timestamp {
unixNano := t.UnixNano()
return &timestamp.Timestamp{
Seconds: int64(unixNano / 1e9),
Nanos: int32(unixNano % 1e9),
}
}
func rowToPoint(v *view.View, row *view.Row, endTimestamp *timestamp.Timestamp, mType measureType) *metricspb.Point {
pt := &metricspb.Point{
Timestamp: endTimestamp,
}
switch data := row.Data.(type) {
case *view.CountData:
pt.Value = &metricspb.Point_Int64Value{Int64Value: data.Value}
case *view.DistributionData:
pt.Value = &metricspb.Point_DistributionValue{
DistributionValue: &metricspb.DistributionValue{
Count: data.Count,
Sum: float64(data.Count) * data.Mean, // because Mean := Sum/Count
// TODO: Add Exemplar
Buckets: bucketsToProtoBuckets(data.CountPerBucket),
BucketOptions: &metricspb.DistributionValue_BucketOptions{
Type: &metricspb.DistributionValue_BucketOptions_Explicit_{
Explicit: &metricspb.DistributionValue_BucketOptions_Explicit{
Bounds: v.Aggregation.Buckets,
},
},
},
SumOfSquaredDeviation: data.SumOfSquaredDev,
}}
case *view.LastValueData:
setPointValue(pt, data.Value, mType)
case *view.SumData:
setPointValue(pt, data.Value, mType)
}
return pt
}
// Not returning anything from this function because metricspb.Point.is_Value is an unexported
// interface hence we just have to set its value by pointer.
func setPointValue(pt *metricspb.Point, value float64, mType measureType) {
if mType == measureInt64 {
pt.Value = &metricspb.Point_Int64Value{Int64Value: int64(value)}
} else {
pt.Value = &metricspb.Point_DoubleValue{DoubleValue: value}
}
}
func bucketsToProtoBuckets(countPerBucket []int64) []*metricspb.DistributionValue_Bucket {
distBuckets := make([]*metricspb.DistributionValue_Bucket, len(countPerBucket))
for i := 0; i < len(countPerBucket); i++ {
count := countPerBucket[i]
distBuckets[i] = &metricspb.DistributionValue_Bucket{
Count: count,
}
}
return distBuckets
}
func labelValuesFromTags(tags []tag.Tag) []*metricspb.LabelValue {
if len(tags) == 0 {
return nil
}
labelValues := make([]*metricspb.LabelValue, 0, len(tags))
for _, tag_ := range tags {
labelValues = append(labelValues, &metricspb.LabelValue{
Value: tag_.Value,
// It is imperative that we set the "HasValue" attribute,
// in order to distinguish missing a label from the empty string.
// https://godoc.org/github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1#LabelValue.HasValue
//
// OpenCensus-Go uses non-pointers for tags as seen by this function's arguments,
// so the best case that we can use to distinguish missing labels/tags from the
// empty string is by checking if the Tag.Key.Name() != "" to indicate that we have
// a value.
HasValue: tag_.Key.Name() != "",
})
}
return labelValues
}

View File

@@ -0,0 +1,17 @@
// Copyright 2018, OpenCensus 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 ocagent
const Version = "0.0.1"

View File

@@ -18,13 +18,18 @@ package dns
// Changes may cause incorrect behavior and will be lost if the code is regenerated.
import (
"context"
"encoding/json"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/Azure/go-autorest/tracing"
"net/http"
)
// The package's fully qualified name.
const fqdn = "github.com/Azure/azure-sdk-for-go/services/dns/mgmt/2017-09-01/dns"
// RecordType enumerates the values for record type.
type RecordType string
@@ -68,6 +73,18 @@ type ARecord struct {
Ipv4Address *string `json:"ipv4Address,omitempty"`
}
// AzureEntityResource the resource model definition for a Azure Resource Manager resource with an etag.
type AzureEntityResource struct {
// Etag - READ-ONLY; Resource Etag.
Etag *string `json:"etag,omitempty"`
// ID - READ-ONLY; Fully qualified resource Id for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
ID *string `json:"id,omitempty"`
// Name - READ-ONLY; The name of the resource
Name *string `json:"name,omitempty"`
// Type - READ-ONLY; The type of the resource. Ex- Microsoft.Compute/virtualMachines or Microsoft.Storage/storageAccounts.
Type *string `json:"type,omitempty"`
}
// CaaRecord a CAA record.
type CaaRecord struct {
// Flags - The flags for this CAA record as an integer between 0 and 255.
@@ -116,6 +133,17 @@ type NsRecord struct {
Nsdname *string `json:"nsdname,omitempty"`
}
// ProxyResource the resource model definition for a ARM proxy resource. It will have everything other than
// required location and tags
type ProxyResource struct {
// ID - READ-ONLY; Fully qualified resource Id for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
ID *string `json:"id,omitempty"`
// Name - READ-ONLY; The name of the resource
Name *string `json:"name,omitempty"`
// Type - READ-ONLY; The type of the resource. Ex- Microsoft.Compute/virtualMachines or Microsoft.Storage/storageAccounts.
Type *string `json:"type,omitempty"`
}
// PtrRecord a PTR record.
type PtrRecord struct {
// Ptrdname - The PTR target domain name for this PTR record.
@@ -125,11 +153,11 @@ type PtrRecord struct {
// RecordSet describes a DNS record set (a collection of DNS records with the same name and type).
type RecordSet struct {
autorest.Response `json:"-"`
// ID - The ID of the record set.
// ID - READ-ONLY; The ID of the record set.
ID *string `json:"id,omitempty"`
// Name - The name of the record set.
// Name - READ-ONLY; The name of the record set.
Name *string `json:"name,omitempty"`
// Type - The type of the record set.
// Type - READ-ONLY; The type of the record set.
Type *string `json:"type,omitempty"`
// Etag - The etag of the record set.
Etag *string `json:"etag,omitempty"`
@@ -140,15 +168,6 @@ type RecordSet struct {
// MarshalJSON is the custom marshaler for RecordSet.
func (rs RecordSet) MarshalJSON() ([]byte, error) {
objectMap := make(map[string]interface{})
if rs.ID != nil {
objectMap["id"] = rs.ID
}
if rs.Name != nil {
objectMap["name"] = rs.Name
}
if rs.Type != nil {
objectMap["type"] = rs.Type
}
if rs.Etag != nil {
objectMap["etag"] = rs.Etag
}
@@ -223,7 +242,7 @@ type RecordSetListResult struct {
autorest.Response `json:"-"`
// Value - Information about the record sets in the response.
Value *[]RecordSet `json:"value,omitempty"`
// NextLink - The continuation token for the next page of results.
// NextLink - READ-ONLY; The continuation token for the next page of results.
NextLink *string `json:"nextLink,omitempty"`
}
@@ -233,14 +252,24 @@ type RecordSetListResultIterator struct {
page RecordSetListResultPage
}
// Next advances to the next value. If there was an error making
// NextWithContext advances to the next value. If there was an error making
// the request the iterator does not advance and the error is returned.
func (iter *RecordSetListResultIterator) Next() error {
func (iter *RecordSetListResultIterator) NextWithContext(ctx context.Context) (err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetListResultIterator.NextWithContext")
defer func() {
sc := -1
if iter.Response().Response.Response != nil {
sc = iter.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
iter.i++
if iter.i < len(iter.page.Values()) {
return nil
}
err := iter.page.Next()
err = iter.page.NextWithContext(ctx)
if err != nil {
iter.i--
return err
@@ -249,6 +278,13 @@ func (iter *RecordSetListResultIterator) Next() error {
return nil
}
// Next advances to the next value. If there was an error making
// the request the iterator does not advance and the error is returned.
// Deprecated: Use NextWithContext() instead.
func (iter *RecordSetListResultIterator) Next() error {
return iter.NextWithContext(context.Background())
}
// NotDone returns true if the enumeration should be started or is not yet complete.
func (iter RecordSetListResultIterator) NotDone() bool {
return iter.page.NotDone() && iter.i < len(iter.page.Values())
@@ -268,6 +304,11 @@ func (iter RecordSetListResultIterator) Value() RecordSet {
return iter.page.Values()[iter.i]
}
// Creates a new instance of the RecordSetListResultIterator type.
func NewRecordSetListResultIterator(page RecordSetListResultPage) RecordSetListResultIterator {
return RecordSetListResultIterator{page: page}
}
// IsEmpty returns true if the ListResult contains no values.
func (rslr RecordSetListResult) IsEmpty() bool {
return rslr.Value == nil || len(*rslr.Value) == 0
@@ -275,11 +316,11 @@ func (rslr RecordSetListResult) IsEmpty() bool {
// recordSetListResultPreparer prepares a request to retrieve the next set of results.
// It returns nil if no more results exist.
func (rslr RecordSetListResult) recordSetListResultPreparer() (*http.Request, error) {
func (rslr RecordSetListResult) recordSetListResultPreparer(ctx context.Context) (*http.Request, error) {
if rslr.NextLink == nil || len(to.String(rslr.NextLink)) < 1 {
return nil, nil
}
return autorest.Prepare(&http.Request{},
return autorest.Prepare((&http.Request{}).WithContext(ctx),
autorest.AsJSON(),
autorest.AsGet(),
autorest.WithBaseURL(to.String(rslr.NextLink)))
@@ -287,14 +328,24 @@ func (rslr RecordSetListResult) recordSetListResultPreparer() (*http.Request, er
// RecordSetListResultPage contains a page of RecordSet values.
type RecordSetListResultPage struct {
fn func(RecordSetListResult) (RecordSetListResult, error)
fn func(context.Context, RecordSetListResult) (RecordSetListResult, error)
rslr RecordSetListResult
}
// Next advances to the next page of values. If there was an error making
// NextWithContext advances to the next page of values. If there was an error making
// the request the page does not advance and the error is returned.
func (page *RecordSetListResultPage) Next() error {
next, err := page.fn(page.rslr)
func (page *RecordSetListResultPage) NextWithContext(ctx context.Context) (err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetListResultPage.NextWithContext")
defer func() {
sc := -1
if page.Response().Response.Response != nil {
sc = page.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
next, err := page.fn(ctx, page.rslr)
if err != nil {
return err
}
@@ -302,6 +353,13 @@ func (page *RecordSetListResultPage) Next() error {
return nil
}
// Next advances to the next page of values. If there was an error making
// the request the page does not advance and the error is returned.
// Deprecated: Use NextWithContext() instead.
func (page *RecordSetListResultPage) Next() error {
return page.NextWithContext(context.Background())
}
// NotDone returns true if the page enumeration should be started or is not yet complete.
func (page RecordSetListResultPage) NotDone() bool {
return !page.rslr.IsEmpty()
@@ -320,13 +378,18 @@ func (page RecordSetListResultPage) Values() []RecordSet {
return *page.rslr.Value
}
// Creates a new instance of the RecordSetListResultPage type.
func NewRecordSetListResultPage(getNextPage func(context.Context, RecordSetListResult) (RecordSetListResult, error)) RecordSetListResultPage {
return RecordSetListResultPage{fn: getNextPage}
}
// RecordSetProperties represents the properties of the records in the record set.
type RecordSetProperties struct {
// Metadata - The metadata attached to the record set.
Metadata map[string]*string `json:"metadata"`
// TTL - The TTL (time-to-live) of the records in the record set.
TTL *int64 `json:"TTL,omitempty"`
// Fqdn - Fully qualified domain name of the record set.
// Fqdn - READ-ONLY; Fully qualified domain name of the record set.
Fqdn *string `json:"fqdn,omitempty"`
// ARecords - The list of A records in the record set.
ARecords *[]ARecord `json:"ARecords,omitempty"`
@@ -359,9 +422,6 @@ func (rsp RecordSetProperties) MarshalJSON() ([]byte, error) {
if rsp.TTL != nil {
objectMap["TTL"] = rsp.TTL
}
if rsp.Fqdn != nil {
objectMap["fqdn"] = rsp.Fqdn
}
if rsp.ARecords != nil {
objectMap["ARecords"] = rsp.ARecords
}
@@ -401,39 +461,14 @@ type RecordSetUpdateParameters struct {
RecordSet *RecordSet `json:"RecordSet,omitempty"`
}
// Resource common properties of an Azure Resource Manager resource
// Resource ...
type Resource struct {
// ID - Resource ID.
// ID - READ-ONLY; Fully qualified resource Id for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
ID *string `json:"id,omitempty"`
// Name - Resource name.
// Name - READ-ONLY; The name of the resource
Name *string `json:"name,omitempty"`
// Type - Resource type.
// Type - READ-ONLY; The type of the resource. Ex- Microsoft.Compute/virtualMachines or Microsoft.Storage/storageAccounts.
Type *string `json:"type,omitempty"`
// Location - Resource location.
Location *string `json:"location,omitempty"`
// Tags - Resource tags.
Tags map[string]*string `json:"tags"`
}
// MarshalJSON is the custom marshaler for Resource.
func (r Resource) MarshalJSON() ([]byte, error) {
objectMap := make(map[string]interface{})
if r.ID != nil {
objectMap["id"] = r.ID
}
if r.Name != nil {
objectMap["name"] = r.Name
}
if r.Type != nil {
objectMap["type"] = r.Type
}
if r.Location != nil {
objectMap["location"] = r.Location
}
if r.Tags != nil {
objectMap["tags"] = r.Tags
}
return json.Marshal(objectMap)
}
// SoaRecord an SOA record.
@@ -472,6 +507,32 @@ type SubResource struct {
ID *string `json:"id,omitempty"`
}
// TrackedResource the resource model definition for a ARM tracked top level resource
type TrackedResource struct {
// Tags - Resource tags.
Tags map[string]*string `json:"tags"`
// Location - The geo-location where the resource lives
Location *string `json:"location,omitempty"`
// ID - READ-ONLY; Fully qualified resource Id for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
ID *string `json:"id,omitempty"`
// Name - READ-ONLY; The name of the resource
Name *string `json:"name,omitempty"`
// Type - READ-ONLY; The type of the resource. Ex- Microsoft.Compute/virtualMachines or Microsoft.Storage/storageAccounts.
Type *string `json:"type,omitempty"`
}
// MarshalJSON is the custom marshaler for TrackedResource.
func (tr TrackedResource) MarshalJSON() ([]byte, error) {
objectMap := make(map[string]interface{})
if tr.Tags != nil {
objectMap["tags"] = tr.Tags
}
if tr.Location != nil {
objectMap["location"] = tr.Location
}
return json.Marshal(objectMap)
}
// TxtRecord a TXT record.
type TxtRecord struct {
// Value - The text value of this TXT record.
@@ -485,16 +546,16 @@ type Zone struct {
Etag *string `json:"etag,omitempty"`
// ZoneProperties - The properties of the zone.
*ZoneProperties `json:"properties,omitempty"`
// ID - Resource ID.
ID *string `json:"id,omitempty"`
// Name - Resource name.
Name *string `json:"name,omitempty"`
// Type - Resource type.
Type *string `json:"type,omitempty"`
// Location - Resource location.
Location *string `json:"location,omitempty"`
// Tags - Resource tags.
Tags map[string]*string `json:"tags"`
// Location - The geo-location where the resource lives
Location *string `json:"location,omitempty"`
// ID - READ-ONLY; Fully qualified resource Id for the resource. Ex - /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/{resourceProviderNamespace}/{resourceType}/{resourceName}
ID *string `json:"id,omitempty"`
// Name - READ-ONLY; The name of the resource
Name *string `json:"name,omitempty"`
// Type - READ-ONLY; The type of the resource. Ex- Microsoft.Compute/virtualMachines or Microsoft.Storage/storageAccounts.
Type *string `json:"type,omitempty"`
}
// MarshalJSON is the custom marshaler for Zone.
@@ -506,21 +567,12 @@ func (z Zone) MarshalJSON() ([]byte, error) {
if z.ZoneProperties != nil {
objectMap["properties"] = z.ZoneProperties
}
if z.ID != nil {
objectMap["id"] = z.ID
}
if z.Name != nil {
objectMap["name"] = z.Name
}
if z.Type != nil {
objectMap["type"] = z.Type
if z.Tags != nil {
objectMap["tags"] = z.Tags
}
if z.Location != nil {
objectMap["location"] = z.Location
}
if z.Tags != nil {
objectMap["tags"] = z.Tags
}
return json.Marshal(objectMap)
}
@@ -551,6 +603,24 @@ func (z *Zone) UnmarshalJSON(body []byte) error {
}
z.ZoneProperties = &zoneProperties
}
case "tags":
if v != nil {
var tags map[string]*string
err = json.Unmarshal(*v, &tags)
if err != nil {
return err
}
z.Tags = tags
}
case "location":
if v != nil {
var location string
err = json.Unmarshal(*v, &location)
if err != nil {
return err
}
z.Location = &location
}
case "id":
if v != nil {
var ID string
@@ -578,24 +648,6 @@ func (z *Zone) UnmarshalJSON(body []byte) error {
}
z.Type = &typeVar
}
case "location":
if v != nil {
var location string
err = json.Unmarshal(*v, &location)
if err != nil {
return err
}
z.Location = &location
}
case "tags":
if v != nil {
var tags map[string]*string
err = json.Unmarshal(*v, &tags)
if err != nil {
return err
}
z.Tags = tags
}
}
}
@@ -607,7 +659,7 @@ type ZoneListResult struct {
autorest.Response `json:"-"`
// Value - Information about the DNS zones.
Value *[]Zone `json:"value,omitempty"`
// NextLink - The continuation token for the next page of results.
// NextLink - READ-ONLY; The continuation token for the next page of results.
NextLink *string `json:"nextLink,omitempty"`
}
@@ -617,14 +669,24 @@ type ZoneListResultIterator struct {
page ZoneListResultPage
}
// Next advances to the next value. If there was an error making
// NextWithContext advances to the next value. If there was an error making
// the request the iterator does not advance and the error is returned.
func (iter *ZoneListResultIterator) Next() error {
func (iter *ZoneListResultIterator) NextWithContext(ctx context.Context) (err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZoneListResultIterator.NextWithContext")
defer func() {
sc := -1
if iter.Response().Response.Response != nil {
sc = iter.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
iter.i++
if iter.i < len(iter.page.Values()) {
return nil
}
err := iter.page.Next()
err = iter.page.NextWithContext(ctx)
if err != nil {
iter.i--
return err
@@ -633,6 +695,13 @@ func (iter *ZoneListResultIterator) Next() error {
return nil
}
// Next advances to the next value. If there was an error making
// the request the iterator does not advance and the error is returned.
// Deprecated: Use NextWithContext() instead.
func (iter *ZoneListResultIterator) Next() error {
return iter.NextWithContext(context.Background())
}
// NotDone returns true if the enumeration should be started or is not yet complete.
func (iter ZoneListResultIterator) NotDone() bool {
return iter.page.NotDone() && iter.i < len(iter.page.Values())
@@ -652,6 +721,11 @@ func (iter ZoneListResultIterator) Value() Zone {
return iter.page.Values()[iter.i]
}
// Creates a new instance of the ZoneListResultIterator type.
func NewZoneListResultIterator(page ZoneListResultPage) ZoneListResultIterator {
return ZoneListResultIterator{page: page}
}
// IsEmpty returns true if the ListResult contains no values.
func (zlr ZoneListResult) IsEmpty() bool {
return zlr.Value == nil || len(*zlr.Value) == 0
@@ -659,11 +733,11 @@ func (zlr ZoneListResult) IsEmpty() bool {
// zoneListResultPreparer prepares a request to retrieve the next set of results.
// It returns nil if no more results exist.
func (zlr ZoneListResult) zoneListResultPreparer() (*http.Request, error) {
func (zlr ZoneListResult) zoneListResultPreparer(ctx context.Context) (*http.Request, error) {
if zlr.NextLink == nil || len(to.String(zlr.NextLink)) < 1 {
return nil, nil
}
return autorest.Prepare(&http.Request{},
return autorest.Prepare((&http.Request{}).WithContext(ctx),
autorest.AsJSON(),
autorest.AsGet(),
autorest.WithBaseURL(to.String(zlr.NextLink)))
@@ -671,14 +745,24 @@ func (zlr ZoneListResult) zoneListResultPreparer() (*http.Request, error) {
// ZoneListResultPage contains a page of Zone values.
type ZoneListResultPage struct {
fn func(ZoneListResult) (ZoneListResult, error)
fn func(context.Context, ZoneListResult) (ZoneListResult, error)
zlr ZoneListResult
}
// Next advances to the next page of values. If there was an error making
// NextWithContext advances to the next page of values. If there was an error making
// the request the page does not advance and the error is returned.
func (page *ZoneListResultPage) Next() error {
next, err := page.fn(page.zlr)
func (page *ZoneListResultPage) NextWithContext(ctx context.Context) (err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZoneListResultPage.NextWithContext")
defer func() {
sc := -1
if page.Response().Response.Response != nil {
sc = page.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
next, err := page.fn(ctx, page.zlr)
if err != nil {
return err
}
@@ -686,6 +770,13 @@ func (page *ZoneListResultPage) Next() error {
return nil
}
// Next advances to the next page of values. If there was an error making
// the request the page does not advance and the error is returned.
// Deprecated: Use NextWithContext() instead.
func (page *ZoneListResultPage) Next() error {
return page.NextWithContext(context.Background())
}
// NotDone returns true if the page enumeration should be started or is not yet complete.
func (page ZoneListResultPage) NotDone() bool {
return !page.zlr.IsEmpty()
@@ -704,60 +795,39 @@ func (page ZoneListResultPage) Values() []Zone {
return *page.zlr.Value
}
// Creates a new instance of the ZoneListResultPage type.
func NewZoneListResultPage(getNextPage func(context.Context, ZoneListResult) (ZoneListResult, error)) ZoneListResultPage {
return ZoneListResultPage{fn: getNextPage}
}
// ZoneProperties represents the properties of the zone.
type ZoneProperties struct {
// MaxNumberOfRecordSets - The maximum number of record sets that can be created in this DNS zone. This is a read-only property and any attempt to set this value will be ignored.
// MaxNumberOfRecordSets - READ-ONLY; The maximum number of record sets that can be created in this DNS zone. This is a read-only property and any attempt to set this value will be ignored.
MaxNumberOfRecordSets *int64 `json:"maxNumberOfRecordSets,omitempty"`
// NumberOfRecordSets - The current number of record sets in this DNS zone. This is a read-only property and any attempt to set this value will be ignored.
// NumberOfRecordSets - READ-ONLY; The current number of record sets in this DNS zone. This is a read-only property and any attempt to set this value will be ignored.
NumberOfRecordSets *int64 `json:"numberOfRecordSets,omitempty"`
// NameServers - The name servers for this DNS zone. This is a read-only property and any attempt to set this value will be ignored.
// NameServers - READ-ONLY; The name servers for this DNS zone. This is a read-only property and any attempt to set this value will be ignored.
NameServers *[]string `json:"nameServers,omitempty"`
}
// ZonesDeleteFuture an abstraction for monitoring and retrieving the results of a long-running operation.
type ZonesDeleteFuture struct {
azure.Future
req *http.Request
}
// Result returns the result of the asynchronous operation.
// If the operation has not completed it will return an error.
func (future ZonesDeleteFuture) Result(client ZonesClient) (ar autorest.Response, err error) {
func (future *ZonesDeleteFuture) Result(client ZonesClient) (ar autorest.Response, err error) {
var done bool
done, err = future.Done(client)
done, err = future.DoneWithContext(context.Background(), client)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.ZonesDeleteFuture", "Result", future.Response(), "Polling failure")
return
}
if !done {
return ar, azure.NewAsyncOpIncompleteError("dns.ZonesDeleteFuture")
}
if future.PollingMethod() == azure.PollingLocation {
ar, err = client.DeleteResponder(future.Response())
if err != nil {
err = autorest.NewErrorWithError(err, "dns.ZonesDeleteFuture", "Result", future.Response(), "Failure responding to request")
}
err = azure.NewAsyncOpIncompleteError("dns.ZonesDeleteFuture")
return
}
var req *http.Request
var resp *http.Response
if future.PollingURL() != "" {
req, err = http.NewRequest(http.MethodGet, future.PollingURL(), nil)
if err != nil {
return
}
} else {
req = autorest.ChangeToGet(future.req)
}
resp, err = autorest.SendWithSender(client, req,
autorest.DoRetryForStatusCodes(client.RetryAttempts, client.RetryDuration, autorest.StatusCodesForRetry...))
if err != nil {
err = autorest.NewErrorWithError(err, "dns.ZonesDeleteFuture", "Result", resp, "Failure sending request")
return
}
ar, err = client.DeleteResponder(resp)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.ZonesDeleteFuture", "Result", resp, "Failure responding to request")
}
ar.Response = future.Response()
return
}

View File

@@ -21,6 +21,8 @@ import (
"context"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/validation"
"github.com/Azure/go-autorest/tracing"
"net/http"
)
@@ -40,15 +42,38 @@ func NewRecordSetsClientWithBaseURI(baseURI string, subscriptionID string) Recor
}
// CreateOrUpdate creates or updates a record set within a DNS zone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). relativeRecordSetName is the name of the record set, relative to the name of the zone. recordType is the
// type of DNS record in this record set. Record sets of type SOA can be updated but not created (they are created
// when the DNS zone is created). parameters is parameters supplied to the CreateOrUpdate operation. ifMatch is the
// etag of the record set. Omit this value to always overwrite the current record set. Specify the last-seen etag
// value to prevent accidentally overwritting any concurrent changes. ifNoneMatch is set to '*' to allow a new
// record set to be created, but to prevent updating an existing record set. Other values will be ignored.
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// relativeRecordSetName - the name of the record set, relative to the name of the zone.
// recordType - the type of DNS record in this record set. Record sets of type SOA can be updated but not
// created (they are created when the DNS zone is created).
// parameters - parameters supplied to the CreateOrUpdate operation.
// ifMatch - the etag of the record set. Omit this value to always overwrite the current record set. Specify
// the last-seen etag value to prevent accidentally overwriting any concurrent changes.
// ifNoneMatch - set to '*' to allow a new record set to be created, but to prevent updating an existing record
// set. Other values will be ignored.
func (client RecordSetsClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType RecordType, parameters RecordSet, ifMatch string, ifNoneMatch string) (result RecordSet, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.CreateOrUpdate")
defer func() {
sc := -1
if result.Response.Response != nil {
sc = result.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.RecordSetsClient", "CreateOrUpdate", err.Error())
}
req, err := client.CreateOrUpdatePreparer(ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType, parameters, ifMatch, ifNoneMatch)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.RecordSetsClient", "CreateOrUpdate", nil, "Failure preparing request")
@@ -85,6 +110,9 @@ func (client RecordSetsClient) CreateOrUpdatePreparer(ctx context.Context, resou
"api-version": APIVersion,
}
parameters.ID = nil
parameters.Name = nil
parameters.Type = nil
preparer := autorest.CreatePreparer(
autorest.AsContentType("application/json; charset=utf-8"),
autorest.AsPut(),
@@ -124,13 +152,35 @@ func (client RecordSetsClient) CreateOrUpdateResponder(resp *http.Response) (res
}
// Delete deletes a record set from a DNS zone. This operation cannot be undone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). relativeRecordSetName is the name of the record set, relative to the name of the zone. recordType is the
// type of DNS record in this record set. Record sets of type SOA cannot be deleted (they are deleted when the DNS
// zone is deleted). ifMatch is the etag of the record set. Omit this value to always delete the current record
// set. Specify the last-seen etag value to prevent accidentally deleting any concurrent changes.
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// relativeRecordSetName - the name of the record set, relative to the name of the zone.
// recordType - the type of DNS record in this record set. Record sets of type SOA cannot be deleted (they are
// deleted when the DNS zone is deleted).
// ifMatch - the etag of the record set. Omit this value to always delete the current record set. Specify the
// last-seen etag value to prevent accidentally deleting any concurrent changes.
func (client RecordSetsClient) Delete(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType RecordType, ifMatch string) (result autorest.Response, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.Delete")
defer func() {
sc := -1
if result.Response != nil {
sc = result.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.RecordSetsClient", "Delete", err.Error())
}
req, err := client.DeletePreparer(ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType, ifMatch)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.RecordSetsClient", "Delete", nil, "Failure preparing request")
@@ -199,11 +249,32 @@ func (client RecordSetsClient) DeleteResponder(resp *http.Response) (result auto
}
// Get gets a record set.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). relativeRecordSetName is the name of the record set, relative to the name of the zone. recordType is the
// type of DNS record in this record set.
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// relativeRecordSetName - the name of the record set, relative to the name of the zone.
// recordType - the type of DNS record in this record set.
func (client RecordSetsClient) Get(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType RecordType) (result RecordSet, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.Get")
defer func() {
sc := -1
if result.Response.Response != nil {
sc = result.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.RecordSetsClient", "Get", err.Error())
}
req, err := client.GetPreparer(ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.RecordSetsClient", "Get", nil, "Failure preparing request")
@@ -269,13 +340,34 @@ func (client RecordSetsClient) GetResponder(resp *http.Response) (result RecordS
}
// ListByDNSZone lists all record sets in a DNS zone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). top is the maximum number of record sets to return. If not specified, returns up to 100 record sets.
// recordsetnamesuffix is the suffix label of the record set name that has to be used to filter the record set
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// top - the maximum number of record sets to return. If not specified, returns up to 100 record sets.
// recordsetnamesuffix - the suffix label of the record set name that has to be used to filter the record set
// enumerations. If this parameter is specified, Enumeration will return only records that end with
// .<recordSetNameSuffix>
func (client RecordSetsClient) ListByDNSZone(ctx context.Context, resourceGroupName string, zoneName string, top *int32, recordsetnamesuffix string) (result RecordSetListResultPage, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.ListByDNSZone")
defer func() {
sc := -1
if result.rslr.Response.Response != nil {
sc = result.rslr.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.RecordSetsClient", "ListByDNSZone", err.Error())
}
result.fn = client.listByDNSZoneNextResults
req, err := client.ListByDNSZonePreparer(ctx, resourceGroupName, zoneName, top, recordsetnamesuffix)
if err != nil {
@@ -346,8 +438,8 @@ func (client RecordSetsClient) ListByDNSZoneResponder(resp *http.Response) (resu
}
// listByDNSZoneNextResults retrieves the next set of results, if any.
func (client RecordSetsClient) listByDNSZoneNextResults(lastResults RecordSetListResult) (result RecordSetListResult, err error) {
req, err := lastResults.recordSetListResultPreparer()
func (client RecordSetsClient) listByDNSZoneNextResults(ctx context.Context, lastResults RecordSetListResult) (result RecordSetListResult, err error) {
req, err := lastResults.recordSetListResultPreparer(ctx)
if err != nil {
return result, autorest.NewErrorWithError(err, "dns.RecordSetsClient", "listByDNSZoneNextResults", nil, "Failure preparing next results request")
}
@@ -368,18 +460,50 @@ func (client RecordSetsClient) listByDNSZoneNextResults(lastResults RecordSetLis
// ListByDNSZoneComplete enumerates all values, automatically crossing page boundaries as required.
func (client RecordSetsClient) ListByDNSZoneComplete(ctx context.Context, resourceGroupName string, zoneName string, top *int32, recordsetnamesuffix string) (result RecordSetListResultIterator, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.ListByDNSZone")
defer func() {
sc := -1
if result.Response().Response.Response != nil {
sc = result.page.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
result.page, err = client.ListByDNSZone(ctx, resourceGroupName, zoneName, top, recordsetnamesuffix)
return
}
// ListByType lists the record sets of a specified type in a DNS zone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). recordType is the type of record sets to enumerate. top is the maximum number of record sets to return. If
// not specified, returns up to 100 record sets. recordsetnamesuffix is the suffix label of the record set name
// that has to be used to filter the record set enumerations. If this parameter is specified, Enumeration will
// return only records that end with .<recordSetNameSuffix>
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// recordType - the type of record sets to enumerate.
// top - the maximum number of record sets to return. If not specified, returns up to 100 record sets.
// recordsetnamesuffix - the suffix label of the record set name that has to be used to filter the record set
// enumerations. If this parameter is specified, Enumeration will return only records that end with
// .<recordSetNameSuffix>
func (client RecordSetsClient) ListByType(ctx context.Context, resourceGroupName string, zoneName string, recordType RecordType, top *int32, recordsetnamesuffix string) (result RecordSetListResultPage, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.ListByType")
defer func() {
sc := -1
if result.rslr.Response.Response != nil {
sc = result.rslr.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.RecordSetsClient", "ListByType", err.Error())
}
result.fn = client.listByTypeNextResults
req, err := client.ListByTypePreparer(ctx, resourceGroupName, zoneName, recordType, top, recordsetnamesuffix)
if err != nil {
@@ -451,8 +575,8 @@ func (client RecordSetsClient) ListByTypeResponder(resp *http.Response) (result
}
// listByTypeNextResults retrieves the next set of results, if any.
func (client RecordSetsClient) listByTypeNextResults(lastResults RecordSetListResult) (result RecordSetListResult, err error) {
req, err := lastResults.recordSetListResultPreparer()
func (client RecordSetsClient) listByTypeNextResults(ctx context.Context, lastResults RecordSetListResult) (result RecordSetListResult, err error) {
req, err := lastResults.recordSetListResultPreparer(ctx)
if err != nil {
return result, autorest.NewErrorWithError(err, "dns.RecordSetsClient", "listByTypeNextResults", nil, "Failure preparing next results request")
}
@@ -473,18 +597,50 @@ func (client RecordSetsClient) listByTypeNextResults(lastResults RecordSetListRe
// ListByTypeComplete enumerates all values, automatically crossing page boundaries as required.
func (client RecordSetsClient) ListByTypeComplete(ctx context.Context, resourceGroupName string, zoneName string, recordType RecordType, top *int32, recordsetnamesuffix string) (result RecordSetListResultIterator, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.ListByType")
defer func() {
sc := -1
if result.Response().Response.Response != nil {
sc = result.page.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
result.page, err = client.ListByType(ctx, resourceGroupName, zoneName, recordType, top, recordsetnamesuffix)
return
}
// Update updates a record set within a DNS zone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). relativeRecordSetName is the name of the record set, relative to the name of the zone. recordType is the
// type of DNS record in this record set. parameters is parameters supplied to the Update operation. ifMatch is the
// etag of the record set. Omit this value to always overwrite the current record set. Specify the last-seen etag
// value to prevent accidentally overwritting concurrent changes.
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// relativeRecordSetName - the name of the record set, relative to the name of the zone.
// recordType - the type of DNS record in this record set.
// parameters - parameters supplied to the Update operation.
// ifMatch - the etag of the record set. Omit this value to always overwrite the current record set. Specify
// the last-seen etag value to prevent accidentally overwriting concurrent changes.
func (client RecordSetsClient) Update(ctx context.Context, resourceGroupName string, zoneName string, relativeRecordSetName string, recordType RecordType, parameters RecordSet, ifMatch string) (result RecordSet, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/RecordSetsClient.Update")
defer func() {
sc := -1
if result.Response.Response != nil {
sc = result.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.RecordSetsClient", "Update", err.Error())
}
req, err := client.UpdatePreparer(ctx, resourceGroupName, zoneName, relativeRecordSetName, recordType, parameters, ifMatch)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.RecordSetsClient", "Update", nil, "Failure preparing request")
@@ -521,6 +677,9 @@ func (client RecordSetsClient) UpdatePreparer(ctx context.Context, resourceGroup
"api-version": APIVersion,
}
parameters.ID = nil
parameters.Name = nil
parameters.Type = nil
preparer := autorest.CreatePreparer(
autorest.AsContentType("application/json; charset=utf-8"),
autorest.AsPatch(),

View File

@@ -21,6 +21,8 @@ import (
"context"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/validation"
"github.com/Azure/go-autorest/tracing"
"net/http"
)
@@ -40,13 +42,35 @@ func NewZonesClientWithBaseURI(baseURI string, subscriptionID string) ZonesClien
}
// CreateOrUpdate creates or updates a DNS zone. Does not modify DNS records within the zone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). parameters is parameters supplied to the CreateOrUpdate operation. ifMatch is the etag of the DNS zone.
// Omit this value to always overwrite the current zone. Specify the last-seen etag value to prevent accidentally
// overwritting any concurrent changes. ifNoneMatch is set to '*' to allow a new DNS zone to be created, but to
// prevent updating an existing zone. Other values will be ignored.
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// parameters - parameters supplied to the CreateOrUpdate operation.
// ifMatch - the etag of the DNS zone. Omit this value to always overwrite the current zone. Specify the
// last-seen etag value to prevent accidentally overwriting any concurrent changes.
// ifNoneMatch - set to '*' to allow a new DNS zone to be created, but to prevent updating an existing zone.
// Other values will be ignored.
func (client ZonesClient) CreateOrUpdate(ctx context.Context, resourceGroupName string, zoneName string, parameters Zone, ifMatch string, ifNoneMatch string) (result Zone, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZonesClient.CreateOrUpdate")
defer func() {
sc := -1
if result.Response.Response != nil {
sc = result.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.ZonesClient", "CreateOrUpdate", err.Error())
}
req, err := client.CreateOrUpdatePreparer(ctx, resourceGroupName, zoneName, parameters, ifMatch, ifNoneMatch)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.ZonesClient", "CreateOrUpdate", nil, "Failure preparing request")
@@ -121,11 +145,32 @@ func (client ZonesClient) CreateOrUpdateResponder(resp *http.Response) (result Z
// Delete deletes a DNS zone. WARNING: All DNS records in the zone will also be deleted. This operation cannot be
// undone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot). ifMatch is the etag of the DNS zone. Omit this value to always delete the current zone. Specify the
// last-seen etag value to prevent accidentally deleting any concurrent changes.
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
// ifMatch - the etag of the DNS zone. Omit this value to always delete the current zone. Specify the last-seen
// etag value to prevent accidentally deleting any concurrent changes.
func (client ZonesClient) Delete(ctx context.Context, resourceGroupName string, zoneName string, ifMatch string) (result ZonesDeleteFuture, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZonesClient.Delete")
defer func() {
sc := -1
if result.Response() != nil {
sc = result.Response().StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.ZonesClient", "Delete", err.Error())
}
req, err := client.DeletePreparer(ctx, resourceGroupName, zoneName, ifMatch)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.ZonesClient", "Delete", nil, "Failure preparing request")
@@ -169,15 +214,13 @@ func (client ZonesClient) DeletePreparer(ctx context.Context, resourceGroupName
// DeleteSender sends the Delete request. The method will close the
// http.Response Body if it receives an error.
func (client ZonesClient) DeleteSender(req *http.Request) (future ZonesDeleteFuture, err error) {
sender := autorest.DecorateSender(client, azure.DoRetryWithRegistration(client.Client))
future.Future = azure.NewFuture(req)
future.req = req
_, err = future.Done(sender)
var resp *http.Response
resp, err = autorest.SendWithSender(client, req,
azure.DoRetryWithRegistration(client.Client))
if err != nil {
return
}
err = autorest.Respond(future.Response(),
azure.WithErrorUnlessStatusCode(http.StatusOK, http.StatusAccepted, http.StatusNoContent))
future.Future, err = azure.NewFutureFromResponse(resp)
return
}
@@ -194,10 +237,30 @@ func (client ZonesClient) DeleteResponder(resp *http.Response) (result autorest.
}
// Get gets a DNS zone. Retrieves the zone properties, but not the record sets within the zone.
//
// resourceGroupName is the name of the resource group. zoneName is the name of the DNS zone (without a terminating
// dot).
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// zoneName - the name of the DNS zone (without a terminating dot).
func (client ZonesClient) Get(ctx context.Context, resourceGroupName string, zoneName string) (result Zone, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZonesClient.Get")
defer func() {
sc := -1
if result.Response.Response != nil {
sc = result.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.ZonesClient", "Get", err.Error())
}
req, err := client.GetPreparer(ctx, resourceGroupName, zoneName)
if err != nil {
err = autorest.NewErrorWithError(err, "dns.ZonesClient", "Get", nil, "Failure preparing request")
@@ -261,9 +324,25 @@ func (client ZonesClient) GetResponder(resp *http.Response) (result Zone, err er
}
// List lists the DNS zones in all resource groups in a subscription.
//
// top is the maximum number of DNS zones to return. If not specified, returns up to 100 zones.
// Parameters:
// top - the maximum number of DNS zones to return. If not specified, returns up to 100 zones.
func (client ZonesClient) List(ctx context.Context, top *int32) (result ZoneListResultPage, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZonesClient.List")
defer func() {
sc := -1
if result.zlr.Response.Response != nil {
sc = result.zlr.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.ZonesClient", "List", err.Error())
}
result.fn = client.listNextResults
req, err := client.ListPreparer(ctx, top)
if err != nil {
@@ -329,8 +408,8 @@ func (client ZonesClient) ListResponder(resp *http.Response) (result ZoneListRes
}
// listNextResults retrieves the next set of results, if any.
func (client ZonesClient) listNextResults(lastResults ZoneListResult) (result ZoneListResult, err error) {
req, err := lastResults.zoneListResultPreparer()
func (client ZonesClient) listNextResults(ctx context.Context, lastResults ZoneListResult) (result ZoneListResult, err error) {
req, err := lastResults.zoneListResultPreparer(ctx)
if err != nil {
return result, autorest.NewErrorWithError(err, "dns.ZonesClient", "listNextResults", nil, "Failure preparing next results request")
}
@@ -351,15 +430,45 @@ func (client ZonesClient) listNextResults(lastResults ZoneListResult) (result Zo
// ListComplete enumerates all values, automatically crossing page boundaries as required.
func (client ZonesClient) ListComplete(ctx context.Context, top *int32) (result ZoneListResultIterator, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZonesClient.List")
defer func() {
sc := -1
if result.Response().Response.Response != nil {
sc = result.page.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
result.page, err = client.List(ctx, top)
return
}
// ListByResourceGroup lists the DNS zones within a resource group.
//
// resourceGroupName is the name of the resource group. top is the maximum number of record sets to return. If not
// specified, returns up to 100 record sets.
// Parameters:
// resourceGroupName - the name of the resource group. The name is case insensitive.
// top - the maximum number of record sets to return. If not specified, returns up to 100 record sets.
func (client ZonesClient) ListByResourceGroup(ctx context.Context, resourceGroupName string, top *int32) (result ZoneListResultPage, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZonesClient.ListByResourceGroup")
defer func() {
sc := -1
if result.zlr.Response.Response != nil {
sc = result.zlr.Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
if err := validation.Validate([]validation.Validation{
{TargetValue: resourceGroupName,
Constraints: []validation.Constraint{{Target: "resourceGroupName", Name: validation.MaxLength, Rule: 90, Chain: nil},
{Target: "resourceGroupName", Name: validation.MinLength, Rule: 1, Chain: nil},
{Target: "resourceGroupName", Name: validation.Pattern, Rule: `^[-\w\._\(\)]+$`, Chain: nil}}},
{TargetValue: client.SubscriptionID,
Constraints: []validation.Constraint{{Target: "client.SubscriptionID", Name: validation.MinLength, Rule: 1, Chain: nil}}}}); err != nil {
return result, validation.NewError("dns.ZonesClient", "ListByResourceGroup", err.Error())
}
result.fn = client.listByResourceGroupNextResults
req, err := client.ListByResourceGroupPreparer(ctx, resourceGroupName, top)
if err != nil {
@@ -426,8 +535,8 @@ func (client ZonesClient) ListByResourceGroupResponder(resp *http.Response) (res
}
// listByResourceGroupNextResults retrieves the next set of results, if any.
func (client ZonesClient) listByResourceGroupNextResults(lastResults ZoneListResult) (result ZoneListResult, err error) {
req, err := lastResults.zoneListResultPreparer()
func (client ZonesClient) listByResourceGroupNextResults(ctx context.Context, lastResults ZoneListResult) (result ZoneListResult, err error) {
req, err := lastResults.zoneListResultPreparer(ctx)
if err != nil {
return result, autorest.NewErrorWithError(err, "dns.ZonesClient", "listByResourceGroupNextResults", nil, "Failure preparing next results request")
}
@@ -448,6 +557,16 @@ func (client ZonesClient) listByResourceGroupNextResults(lastResults ZoneListRes
// ListByResourceGroupComplete enumerates all values, automatically crossing page boundaries as required.
func (client ZonesClient) ListByResourceGroupComplete(ctx context.Context, resourceGroupName string, top *int32) (result ZoneListResultIterator, err error) {
if tracing.IsEnabled() {
ctx = tracing.StartSpan(ctx, fqdn+"/ZonesClient.ListByResourceGroup")
defer func() {
sc := -1
if result.Response().Response.Response != nil {
sc = result.page.Response().Response.Response.StatusCode
}
tracing.EndSpan(ctx, sc, err)
}()
}
result.page, err = client.ListByResourceGroup(ctx, resourceGroupName, top)
return
}

View File

@@ -18,4 +18,4 @@ package version
// Changes may cause incorrect behavior and will be lost if the code is regenerated.
// Number contains the semantic version of this SDK.
const Number = "v15.0.1"
const Number = "v31.2.0"

View File

@@ -15,21 +15,22 @@ package adal
// limitations under the License.
import (
"errors"
"fmt"
"net/url"
)
const (
activeDirectoryAPIVersion = "1.0"
activeDirectoryEndpointTemplate = "%s/oauth2/%s%s"
)
// OAuthConfig represents the endpoints needed
// in OAuth operations
type OAuthConfig struct {
AuthorityEndpoint url.URL
AuthorizeEndpoint url.URL
TokenEndpoint url.URL
DeviceCodeEndpoint url.URL
AuthorityEndpoint url.URL `json:"authorityEndpoint"`
AuthorizeEndpoint url.URL `json:"authorizeEndpoint"`
TokenEndpoint url.URL `json:"tokenEndpoint"`
DeviceCodeEndpoint url.URL `json:"deviceCodeEndpoint"`
}
// IsZero returns true if the OAuthConfig object is zero-initialized.
@@ -46,11 +47,24 @@ func validateStringParam(param, name string) error {
// NewOAuthConfig returns an OAuthConfig with tenant specific urls
func NewOAuthConfig(activeDirectoryEndpoint, tenantID string) (*OAuthConfig, error) {
apiVer := "1.0"
return NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, tenantID, &apiVer)
}
// NewOAuthConfigWithAPIVersion returns an OAuthConfig with tenant specific urls.
// If apiVersion is not nil the "api-version" query parameter will be appended to the endpoint URLs with the specified value.
func NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, tenantID string, apiVersion *string) (*OAuthConfig, error) {
if err := validateStringParam(activeDirectoryEndpoint, "activeDirectoryEndpoint"); err != nil {
return nil, err
}
api := ""
// it's legal for tenantID to be empty so don't validate it
const activeDirectoryEndpointTemplate = "%s/oauth2/%s?api-version=%s"
if apiVersion != nil {
if err := validateStringParam(*apiVersion, "apiVersion"); err != nil {
return nil, err
}
api = fmt.Sprintf("?api-version=%s", *apiVersion)
}
u, err := url.Parse(activeDirectoryEndpoint)
if err != nil {
return nil, err
@@ -59,15 +73,15 @@ func NewOAuthConfig(activeDirectoryEndpoint, tenantID string) (*OAuthConfig, err
if err != nil {
return nil, err
}
authorizeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "authorize", activeDirectoryAPIVersion))
authorizeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "authorize", api))
if err != nil {
return nil, err
}
tokenURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "token", activeDirectoryAPIVersion))
tokenURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "token", api))
if err != nil {
return nil, err
}
deviceCodeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "devicecode", activeDirectoryAPIVersion))
deviceCodeURL, err := u.Parse(fmt.Sprintf(activeDirectoryEndpointTemplate, tenantID, "devicecode", api))
if err != nil {
return nil, err
}
@@ -79,3 +93,59 @@ func NewOAuthConfig(activeDirectoryEndpoint, tenantID string) (*OAuthConfig, err
DeviceCodeEndpoint: *deviceCodeURL,
}, nil
}
// MultiTenantOAuthConfig provides endpoints for primary and aulixiary tenant IDs.
type MultiTenantOAuthConfig interface {
PrimaryTenant() *OAuthConfig
AuxiliaryTenants() []*OAuthConfig
}
// OAuthOptions contains optional OAuthConfig creation arguments.
type OAuthOptions struct {
APIVersion string
}
func (c OAuthOptions) apiVersion() string {
if c.APIVersion != "" {
return fmt.Sprintf("?api-version=%s", c.APIVersion)
}
return "1.0"
}
// NewMultiTenantOAuthConfig creates an object that support multitenant OAuth configuration.
// See https://docs.microsoft.com/en-us/azure/azure-resource-manager/authenticate-multi-tenant for more information.
func NewMultiTenantOAuthConfig(activeDirectoryEndpoint, primaryTenantID string, auxiliaryTenantIDs []string, options OAuthOptions) (MultiTenantOAuthConfig, error) {
if len(auxiliaryTenantIDs) == 0 || len(auxiliaryTenantIDs) > 3 {
return nil, errors.New("must specify one to three auxiliary tenants")
}
mtCfg := multiTenantOAuthConfig{
cfgs: make([]*OAuthConfig, len(auxiliaryTenantIDs)+1),
}
apiVer := options.apiVersion()
pri, err := NewOAuthConfigWithAPIVersion(activeDirectoryEndpoint, primaryTenantID, &apiVer)
if err != nil {
return nil, fmt.Errorf("failed to create OAuthConfig for primary tenant: %v", err)
}
mtCfg.cfgs[0] = pri
for i := range auxiliaryTenantIDs {
aux, err := NewOAuthConfig(activeDirectoryEndpoint, auxiliaryTenantIDs[i])
if err != nil {
return nil, fmt.Errorf("failed to create OAuthConfig for tenant '%s': %v", auxiliaryTenantIDs[i], err)
}
mtCfg.cfgs[i+1] = aux
}
return mtCfg, nil
}
type multiTenantOAuthConfig struct {
// first config in the slice is the primary tenant
cfgs []*OAuthConfig
}
func (m multiTenantOAuthConfig) PrimaryTenant() *OAuthConfig {
return m.cfgs[0]
}
func (m multiTenantOAuthConfig) AuxiliaryTenants() []*OAuthConfig {
return m.cfgs[1:]
}

View File

@@ -38,7 +38,7 @@ func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) {
return sf(r)
}
// SendDecorator takes and possibily decorates, by wrapping, a Sender. Decorators may affect the
// SendDecorator takes and possibly decorates, by wrapping, a Sender. Decorators may affect the
// http.Request and pass it along or, first, pass the http.Request along then react to the
// http.Response result.
type SendDecorator func(Sender) Sender

View File

@@ -15,22 +15,26 @@ package adal
// limitations under the License.
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"math"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
"github.com/Azure/go-autorest/autorest/date"
"github.com/Azure/go-autorest/tracing"
"github.com/dgrijalva/jwt-go"
)
@@ -57,6 +61,9 @@ const (
// msiEndpoint is the well known endpoint for getting MSI authentications tokens
msiEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
// the default number of attempts to refresh an MSI authentication token
defaultMaxMSIRefreshAttempts = 5
)
// OAuthTokenProvider is an interface which should be implemented by an access token retriever
@@ -64,6 +71,12 @@ type OAuthTokenProvider interface {
OAuthToken() string
}
// MultitenantOAuthTokenProvider provides tokens used for multi-tenant authorization.
type MultitenantOAuthTokenProvider interface {
PrimaryOAuthToken() string
AuxiliaryOAuthTokens() []string
}
// TokenRefreshError is an interface used by errors returned during token refresh.
type TokenRefreshError interface {
error
@@ -77,23 +90,39 @@ type Refresher interface {
EnsureFresh() error
}
// RefresherWithContext is an interface for token refresh functionality
type RefresherWithContext interface {
RefreshWithContext(ctx context.Context) error
RefreshExchangeWithContext(ctx context.Context, resource string) error
EnsureFreshWithContext(ctx context.Context) error
}
// TokenRefreshCallback is the type representing callbacks that will be called after
// a successful token refresh
type TokenRefreshCallback func(Token) error
// Token encapsulates the access token used to authorize Azure requests.
// https://docs.microsoft.com/en-us/azure/active-directory/develop/v1-oauth2-client-creds-grant-flow#service-to-service-access-token-response
type Token struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn string `json:"expires_in"`
ExpiresOn string `json:"expires_on"`
NotBefore string `json:"not_before"`
ExpiresIn json.Number `json:"expires_in"`
ExpiresOn json.Number `json:"expires_on"`
NotBefore json.Number `json:"not_before"`
Resource string `json:"resource"`
Type string `json:"token_type"`
}
func newToken() Token {
return Token{
ExpiresIn: "0",
ExpiresOn: "0",
NotBefore: "0",
}
}
// IsZero returns true if the token object is zero-initialized.
func (t Token) IsZero() bool {
return t == Token{}
@@ -101,12 +130,12 @@ func (t Token) IsZero() bool {
// Expires returns the time.Time when the Token expires.
func (t Token) Expires() time.Time {
s, err := strconv.Atoi(t.ExpiresOn)
s, err := t.ExpiresOn.Float64()
if err != nil {
s = -3600
}
expiration := date.NewUnixTimeFromSeconds(float64(s))
expiration := date.NewUnixTimeFromSeconds(s)
return time.Time(expiration).UTC()
}
@@ -127,6 +156,12 @@ func (t *Token) OAuthToken() string {
return t.AccessToken
}
// ServicePrincipalSecret is an interface that allows various secret mechanism to fill the form
// that is submitted when acquiring an oAuth token.
type ServicePrincipalSecret interface {
SetAuthenticationValues(spt *ServicePrincipalToken, values *url.Values) error
}
// ServicePrincipalNoSecret represents a secret type that contains no secret
// meaning it is not valid for fetching a fresh token. This is used by Manual
type ServicePrincipalNoSecret struct {
@@ -138,15 +173,19 @@ func (noSecret *ServicePrincipalNoSecret) SetAuthenticationValues(spt *ServicePr
return fmt.Errorf("Manually created ServicePrincipalToken does not contain secret material to retrieve a new access token")
}
// ServicePrincipalSecret is an interface that allows various secret mechanism to fill the form
// that is submitted when acquiring an oAuth token.
type ServicePrincipalSecret interface {
SetAuthenticationValues(spt *ServicePrincipalToken, values *url.Values) error
// MarshalJSON implements the json.Marshaler interface.
func (noSecret ServicePrincipalNoSecret) MarshalJSON() ([]byte, error) {
type tokenType struct {
Type string `json:"type"`
}
return json.Marshal(tokenType{
Type: "ServicePrincipalNoSecret",
})
}
// ServicePrincipalTokenSecret implements ServicePrincipalSecret for client_secret type authorization.
type ServicePrincipalTokenSecret struct {
ClientSecret string
ClientSecret string `json:"value"`
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
@@ -156,49 +195,24 @@ func (tokenSecret *ServicePrincipalTokenSecret) SetAuthenticationValues(spt *Ser
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (tokenSecret ServicePrincipalTokenSecret) MarshalJSON() ([]byte, error) {
type tokenType struct {
Type string `json:"type"`
Value string `json:"value"`
}
return json.Marshal(tokenType{
Type: "ServicePrincipalTokenSecret",
Value: tokenSecret.ClientSecret,
})
}
// ServicePrincipalCertificateSecret implements ServicePrincipalSecret for generic RSA cert auth with signed JWTs.
type ServicePrincipalCertificateSecret struct {
Certificate *x509.Certificate
PrivateKey *rsa.PrivateKey
}
// ServicePrincipalMSISecret implements ServicePrincipalSecret for machines running the MSI Extension.
type ServicePrincipalMSISecret struct {
}
// ServicePrincipalUsernamePasswordSecret implements ServicePrincipalSecret for username and password auth.
type ServicePrincipalUsernamePasswordSecret struct {
Username string
Password string
}
// ServicePrincipalAuthorizationCodeSecret implements ServicePrincipalSecret for authorization code auth.
type ServicePrincipalAuthorizationCodeSecret struct {
ClientSecret string
AuthorizationCode string
RedirectURI string
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
func (secret *ServicePrincipalAuthorizationCodeSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
v.Set("code", secret.AuthorizationCode)
v.Set("client_secret", secret.ClientSecret)
v.Set("redirect_uri", secret.RedirectURI)
return nil
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
func (secret *ServicePrincipalUsernamePasswordSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
v.Set("username", secret.Username)
v.Set("password", secret.Password)
return nil
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
func (msiSecret *ServicePrincipalMSISecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
return nil
}
// SignJwt returns the JWT signed with the certificate's private key.
func (secret *ServicePrincipalCertificateSecret) SignJwt(spt *ServicePrincipalToken) (string, error) {
hasher := sha1.New()
@@ -218,10 +232,12 @@ func (secret *ServicePrincipalCertificateSecret) SignJwt(spt *ServicePrincipalTo
token := jwt.New(jwt.SigningMethodRS256)
token.Header["x5t"] = thumbprint
x5c := []string{base64.StdEncoding.EncodeToString(secret.Certificate.Raw)}
token.Header["x5c"] = x5c
token.Claims = jwt.MapClaims{
"aud": spt.oauthConfig.TokenEndpoint.String(),
"iss": spt.clientID,
"sub": spt.clientID,
"aud": spt.inner.OauthConfig.TokenEndpoint.String(),
"iss": spt.inner.ClientID,
"sub": spt.inner.ClientID,
"jti": base64.URLEncoding.EncodeToString(jti),
"nbf": time.Now().Unix(),
"exp": time.Now().Add(time.Hour * 24).Unix(),
@@ -244,19 +260,156 @@ func (secret *ServicePrincipalCertificateSecret) SetAuthenticationValues(spt *Se
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (secret ServicePrincipalCertificateSecret) MarshalJSON() ([]byte, error) {
return nil, errors.New("marshalling ServicePrincipalCertificateSecret is not supported")
}
// ServicePrincipalMSISecret implements ServicePrincipalSecret for machines running the MSI Extension.
type ServicePrincipalMSISecret struct {
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
func (msiSecret *ServicePrincipalMSISecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (msiSecret ServicePrincipalMSISecret) MarshalJSON() ([]byte, error) {
return nil, errors.New("marshalling ServicePrincipalMSISecret is not supported")
}
// ServicePrincipalUsernamePasswordSecret implements ServicePrincipalSecret for username and password auth.
type ServicePrincipalUsernamePasswordSecret struct {
Username string `json:"username"`
Password string `json:"password"`
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
func (secret *ServicePrincipalUsernamePasswordSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
v.Set("username", secret.Username)
v.Set("password", secret.Password)
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (secret ServicePrincipalUsernamePasswordSecret) MarshalJSON() ([]byte, error) {
type tokenType struct {
Type string `json:"type"`
Username string `json:"username"`
Password string `json:"password"`
}
return json.Marshal(tokenType{
Type: "ServicePrincipalUsernamePasswordSecret",
Username: secret.Username,
Password: secret.Password,
})
}
// ServicePrincipalAuthorizationCodeSecret implements ServicePrincipalSecret for authorization code auth.
type ServicePrincipalAuthorizationCodeSecret struct {
ClientSecret string `json:"value"`
AuthorizationCode string `json:"authCode"`
RedirectURI string `json:"redirect"`
}
// SetAuthenticationValues is a method of the interface ServicePrincipalSecret.
func (secret *ServicePrincipalAuthorizationCodeSecret) SetAuthenticationValues(spt *ServicePrincipalToken, v *url.Values) error {
v.Set("code", secret.AuthorizationCode)
v.Set("client_secret", secret.ClientSecret)
v.Set("redirect_uri", secret.RedirectURI)
return nil
}
// MarshalJSON implements the json.Marshaler interface.
func (secret ServicePrincipalAuthorizationCodeSecret) MarshalJSON() ([]byte, error) {
type tokenType struct {
Type string `json:"type"`
Value string `json:"value"`
AuthCode string `json:"authCode"`
Redirect string `json:"redirect"`
}
return json.Marshal(tokenType{
Type: "ServicePrincipalAuthorizationCodeSecret",
Value: secret.ClientSecret,
AuthCode: secret.AuthorizationCode,
Redirect: secret.RedirectURI,
})
}
// ServicePrincipalToken encapsulates a Token created for a Service Principal.
type ServicePrincipalToken struct {
token Token
secret ServicePrincipalSecret
oauthConfig OAuthConfig
clientID string
resource string
autoRefresh bool
refreshLock *sync.RWMutex
refreshWithin time.Duration
sender Sender
inner servicePrincipalToken
refreshLock *sync.RWMutex
sender Sender
refreshCallbacks []TokenRefreshCallback
// MaxMSIRefreshAttempts is the maximum number of attempts to refresh an MSI token.
MaxMSIRefreshAttempts int
}
// MarshalTokenJSON returns the marshalled inner token.
func (spt ServicePrincipalToken) MarshalTokenJSON() ([]byte, error) {
return json.Marshal(spt.inner.Token)
}
// SetRefreshCallbacks replaces any existing refresh callbacks with the specified callbacks.
func (spt *ServicePrincipalToken) SetRefreshCallbacks(callbacks []TokenRefreshCallback) {
spt.refreshCallbacks = callbacks
}
// MarshalJSON implements the json.Marshaler interface.
func (spt ServicePrincipalToken) MarshalJSON() ([]byte, error) {
return json.Marshal(spt.inner)
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (spt *ServicePrincipalToken) UnmarshalJSON(data []byte) error {
// need to determine the token type
raw := map[string]interface{}{}
err := json.Unmarshal(data, &raw)
if err != nil {
return err
}
secret := raw["secret"].(map[string]interface{})
switch secret["type"] {
case "ServicePrincipalNoSecret":
spt.inner.Secret = &ServicePrincipalNoSecret{}
case "ServicePrincipalTokenSecret":
spt.inner.Secret = &ServicePrincipalTokenSecret{}
case "ServicePrincipalCertificateSecret":
return errors.New("unmarshalling ServicePrincipalCertificateSecret is not supported")
case "ServicePrincipalMSISecret":
return errors.New("unmarshalling ServicePrincipalMSISecret is not supported")
case "ServicePrincipalUsernamePasswordSecret":
spt.inner.Secret = &ServicePrincipalUsernamePasswordSecret{}
case "ServicePrincipalAuthorizationCodeSecret":
spt.inner.Secret = &ServicePrincipalAuthorizationCodeSecret{}
default:
return fmt.Errorf("unrecognized token type '%s'", secret["type"])
}
err = json.Unmarshal(data, &spt.inner)
if err != nil {
return err
}
// Don't override the refreshLock or the sender if those have been already set.
if spt.refreshLock == nil {
spt.refreshLock = &sync.RWMutex{}
}
if spt.sender == nil {
spt.sender = &http.Client{Transport: tracing.Transport}
}
return nil
}
// internal type used for marshalling/unmarshalling
type servicePrincipalToken struct {
Token Token `json:"token"`
Secret ServicePrincipalSecret `json:"secret"`
OauthConfig OAuthConfig `json:"oauth"`
ClientID string `json:"clientID"`
Resource string `json:"resource"`
AutoRefresh bool `json:"autoRefresh"`
RefreshWithin time.Duration `json:"refreshWithin"`
}
func validateOAuthConfig(oac OAuthConfig) error {
@@ -281,14 +434,17 @@ func NewServicePrincipalTokenWithSecret(oauthConfig OAuthConfig, id string, reso
return nil, fmt.Errorf("parameter 'secret' cannot be nil")
}
spt := &ServicePrincipalToken{
oauthConfig: oauthConfig,
secret: secret,
clientID: id,
resource: resource,
autoRefresh: true,
inner: servicePrincipalToken{
Token: newToken(),
OauthConfig: oauthConfig,
Secret: secret,
ClientID: id,
Resource: resource,
AutoRefresh: true,
RefreshWithin: defaultRefresh,
},
refreshLock: &sync.RWMutex{},
refreshWithin: defaultRefresh,
sender: &http.Client{},
sender: &http.Client{Transport: tracing.Transport},
refreshCallbacks: callbacks,
}
return spt, nil
@@ -318,7 +474,39 @@ func NewServicePrincipalTokenFromManualToken(oauthConfig OAuthConfig, clientID s
return nil, err
}
spt.token = token
spt.inner.Token = token
return spt, nil
}
// NewServicePrincipalTokenFromManualTokenSecret creates a ServicePrincipalToken using the supplied token and secret
func NewServicePrincipalTokenFromManualTokenSecret(oauthConfig OAuthConfig, clientID string, resource string, token Token, secret ServicePrincipalSecret, callbacks ...TokenRefreshCallback) (*ServicePrincipalToken, error) {
if err := validateOAuthConfig(oauthConfig); err != nil {
return nil, err
}
if err := validateStringParam(clientID, "clientID"); err != nil {
return nil, err
}
if err := validateStringParam(resource, "resource"); err != nil {
return nil, err
}
if secret == nil {
return nil, fmt.Errorf("parameter 'secret' cannot be nil")
}
if token.IsZero() {
return nil, fmt.Errorf("parameter 'token' cannot be zero-initialized")
}
spt, err := NewServicePrincipalTokenWithSecret(
oauthConfig,
clientID,
resource,
secret,
callbacks...)
if err != nil {
return nil, err
}
spt.inner.Token = token
return spt, nil
}
@@ -486,20 +674,24 @@ func newServicePrincipalTokenFromMSI(msiEndpoint, resource string, userAssignedI
msiEndpointURL.RawQuery = v.Encode()
spt := &ServicePrincipalToken{
oauthConfig: OAuthConfig{
TokenEndpoint: *msiEndpointURL,
inner: servicePrincipalToken{
Token: newToken(),
OauthConfig: OAuthConfig{
TokenEndpoint: *msiEndpointURL,
},
Secret: &ServicePrincipalMSISecret{},
Resource: resource,
AutoRefresh: true,
RefreshWithin: defaultRefresh,
},
secret: &ServicePrincipalMSISecret{},
resource: resource,
autoRefresh: true,
refreshLock: &sync.RWMutex{},
refreshWithin: defaultRefresh,
sender: &http.Client{},
refreshCallbacks: callbacks,
refreshLock: &sync.RWMutex{},
sender: &http.Client{Transport: tracing.Transport},
refreshCallbacks: callbacks,
MaxMSIRefreshAttempts: defaultMaxMSIRefreshAttempts,
}
if userAssignedID != nil {
spt.clientID = *userAssignedID
spt.inner.ClientID = *userAssignedID
}
return spt, nil
@@ -528,12 +720,18 @@ func newTokenRefreshError(message string, resp *http.Response) TokenRefreshError
// EnsureFresh will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (spt *ServicePrincipalToken) EnsureFresh() error {
if spt.autoRefresh && spt.token.WillExpireIn(spt.refreshWithin) {
return spt.EnsureFreshWithContext(context.Background())
}
// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (spt *ServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
if spt.inner.AutoRefresh && spt.inner.Token.WillExpireIn(spt.inner.RefreshWithin) {
// take the write lock then check to see if the token was already refreshed
spt.refreshLock.Lock()
defer spt.refreshLock.Unlock()
if spt.token.WillExpireIn(spt.refreshWithin) {
return spt.refreshInternal(spt.resource)
if spt.inner.Token.WillExpireIn(spt.inner.RefreshWithin) {
return spt.refreshInternal(ctx, spt.inner.Resource)
}
}
return nil
@@ -543,7 +741,7 @@ func (spt *ServicePrincipalToken) EnsureFresh() error {
func (spt *ServicePrincipalToken) InvokeRefreshCallbacks(token Token) error {
if spt.refreshCallbacks != nil {
for _, callback := range spt.refreshCallbacks {
err := callback(spt.token)
err := callback(spt.inner.Token)
if err != nil {
return fmt.Errorf("adal: TokenRefreshCallback handler failed. Error = '%v'", err)
}
@@ -555,21 +753,33 @@ func (spt *ServicePrincipalToken) InvokeRefreshCallbacks(token Token) error {
// Refresh obtains a fresh token for the Service Principal.
// This method is not safe for concurrent use and should be syncrhonized.
func (spt *ServicePrincipalToken) Refresh() error {
return spt.RefreshWithContext(context.Background())
}
// RefreshWithContext obtains a fresh token for the Service Principal.
// This method is not safe for concurrent use and should be syncrhonized.
func (spt *ServicePrincipalToken) RefreshWithContext(ctx context.Context) error {
spt.refreshLock.Lock()
defer spt.refreshLock.Unlock()
return spt.refreshInternal(spt.resource)
return spt.refreshInternal(ctx, spt.inner.Resource)
}
// RefreshExchange refreshes the token, but for a different resource.
// This method is not safe for concurrent use and should be syncrhonized.
func (spt *ServicePrincipalToken) RefreshExchange(resource string) error {
return spt.RefreshExchangeWithContext(context.Background(), resource)
}
// RefreshExchangeWithContext refreshes the token, but for a different resource.
// This method is not safe for concurrent use and should be syncrhonized.
func (spt *ServicePrincipalToken) RefreshExchangeWithContext(ctx context.Context, resource string) error {
spt.refreshLock.Lock()
defer spt.refreshLock.Unlock()
return spt.refreshInternal(resource)
return spt.refreshInternal(ctx, resource)
}
func (spt *ServicePrincipalToken) getGrantType() string {
switch spt.secret.(type) {
switch spt.inner.Secret.(type) {
case *ServicePrincipalUsernamePasswordSecret:
return OAuthGrantTypeUserPass
case *ServicePrincipalAuthorizationCodeSecret:
@@ -587,23 +797,32 @@ func isIMDS(u url.URL) bool {
return u.Host == imds.Host && u.Path == imds.Path
}
func (spt *ServicePrincipalToken) refreshInternal(resource string) error {
req, err := http.NewRequest(http.MethodPost, spt.oauthConfig.TokenEndpoint.String(), nil)
func (spt *ServicePrincipalToken) refreshInternal(ctx context.Context, resource string) error {
req, err := http.NewRequest(http.MethodPost, spt.inner.OauthConfig.TokenEndpoint.String(), nil)
if err != nil {
return fmt.Errorf("adal: Failed to build the refresh request. Error = '%v'", err)
}
if !isIMDS(spt.oauthConfig.TokenEndpoint) {
req.Header.Add("User-Agent", UserAgent())
req = req.WithContext(ctx)
if !isIMDS(spt.inner.OauthConfig.TokenEndpoint) {
v := url.Values{}
v.Set("client_id", spt.clientID)
v.Set("client_id", spt.inner.ClientID)
v.Set("resource", resource)
if spt.token.RefreshToken != "" {
if spt.inner.Token.RefreshToken != "" {
v.Set("grant_type", OAuthGrantTypeRefreshToken)
v.Set("refresh_token", spt.token.RefreshToken)
v.Set("refresh_token", spt.inner.Token.RefreshToken)
// web apps must specify client_secret when refreshing tokens
// see https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-protocols-oauth-code#refreshing-the-access-tokens
if spt.getGrantType() == OAuthGrantTypeAuthorizationCode {
err := spt.inner.Secret.SetAuthenticationValues(spt, &v)
if err != nil {
return err
}
}
} else {
v.Set("grant_type", spt.getGrantType())
err := spt.secret.SetAuthenticationValues(spt, &v)
err := spt.inner.Secret.SetAuthenticationValues(spt, &v)
if err != nil {
return err
}
@@ -616,14 +835,19 @@ func (spt *ServicePrincipalToken) refreshInternal(resource string) error {
req.Body = body
}
if _, ok := spt.secret.(*ServicePrincipalMSISecret); ok {
if _, ok := spt.inner.Secret.(*ServicePrincipalMSISecret); ok {
req.Method = http.MethodGet
req.Header.Set(metadataHeader, "true")
}
resp, err := spt.sender.Do(req)
var resp *http.Response
if isIMDS(spt.inner.OauthConfig.TokenEndpoint) {
resp, err = retryForIMDS(spt.sender, req, spt.MaxMSIRefreshAttempts)
} else {
resp, err = spt.sender.Do(req)
}
if err != nil {
return fmt.Errorf("adal: Failed to execute the refresh request. Error = '%v'", err)
return newTokenRefreshError(fmt.Sprintf("adal: Failed to execute the refresh request. Error = '%v'", err), nil)
}
defer resp.Body.Close()
@@ -631,11 +855,15 @@ func (spt *ServicePrincipalToken) refreshInternal(resource string) error {
if resp.StatusCode != http.StatusOK {
if err != nil {
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Failed reading response body", resp.StatusCode), resp)
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Failed reading response body: %v", resp.StatusCode, err), resp)
}
return newTokenRefreshError(fmt.Sprintf("adal: Refresh request failed. Status Code = '%d'. Response body: %s", resp.StatusCode, string(rb)), resp)
}
// for the following error cases don't return a TokenRefreshError. the operation succeeded
// but some transient failure happened during deserialization. by returning a generic error
// the retry logic will kick in (we don't retry on TokenRefreshError).
if err != nil {
return fmt.Errorf("adal: Failed to read a new service principal token during refresh. Error = '%v'", err)
}
@@ -648,20 +876,99 @@ func (spt *ServicePrincipalToken) refreshInternal(resource string) error {
return fmt.Errorf("adal: Failed to unmarshal the service principal token during refresh. Error = '%v' JSON = '%s'", err, string(rb))
}
spt.token = token
spt.inner.Token = token
return spt.InvokeRefreshCallbacks(token)
}
// retry logic specific to retrieving a token from the IMDS endpoint
func retryForIMDS(sender Sender, req *http.Request, maxAttempts int) (resp *http.Response, err error) {
// copied from client.go due to circular dependency
retries := []int{
http.StatusRequestTimeout, // 408
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout, // 504
}
// extra retry status codes specific to IMDS
retries = append(retries,
http.StatusNotFound,
http.StatusGone,
// all remaining 5xx
http.StatusNotImplemented,
http.StatusHTTPVersionNotSupported,
http.StatusVariantAlsoNegotiates,
http.StatusInsufficientStorage,
http.StatusLoopDetected,
http.StatusNotExtended,
http.StatusNetworkAuthenticationRequired)
// see https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/how-to-use-vm-token#retry-guidance
const maxDelay time.Duration = 60 * time.Second
attempt := 0
delay := time.Duration(0)
for attempt < maxAttempts {
resp, err = sender.Do(req)
// retry on temporary network errors, e.g. transient network failures.
// if we don't receive a response then assume we can't connect to the
// endpoint so we're likely not running on an Azure VM so don't retry.
if (err != nil && !isTemporaryNetworkError(err)) || resp == nil || resp.StatusCode == http.StatusOK || !containsInt(retries, resp.StatusCode) {
return
}
// perform exponential backoff with a cap.
// must increment attempt before calculating delay.
attempt++
// the base value of 2 is the "delta backoff" as specified in the guidance doc
delay += (time.Duration(math.Pow(2, float64(attempt))) * time.Second)
if delay > maxDelay {
delay = maxDelay
}
select {
case <-time.After(delay):
// intentionally left blank
case <-req.Context().Done():
err = req.Context().Err()
return
}
}
return
}
// returns true if the specified error is a temporary network error or false if it's not.
// if the error doesn't implement the net.Error interface the return value is true.
func isTemporaryNetworkError(err error) bool {
if netErr, ok := err.(net.Error); !ok || (ok && netErr.Temporary()) {
return true
}
return false
}
// returns true if slice ints contains the value n
func containsInt(ints []int, n int) bool {
for _, i := range ints {
if i == n {
return true
}
}
return false
}
// SetAutoRefresh enables or disables automatic refreshing of stale tokens.
func (spt *ServicePrincipalToken) SetAutoRefresh(autoRefresh bool) {
spt.autoRefresh = autoRefresh
spt.inner.AutoRefresh = autoRefresh
}
// SetRefreshWithin sets the interval within which if the token will expire, EnsureFresh will
// refresh the token.
func (spt *ServicePrincipalToken) SetRefreshWithin(d time.Duration) {
spt.refreshWithin = d
spt.inner.RefreshWithin = d
return
}
@@ -673,12 +980,76 @@ func (spt *ServicePrincipalToken) SetSender(s Sender) { spt.sender = s }
func (spt *ServicePrincipalToken) OAuthToken() string {
spt.refreshLock.RLock()
defer spt.refreshLock.RUnlock()
return spt.token.OAuthToken()
return spt.inner.Token.OAuthToken()
}
// Token returns a copy of the current token.
func (spt *ServicePrincipalToken) Token() Token {
spt.refreshLock.RLock()
defer spt.refreshLock.RUnlock()
return spt.token
return spt.inner.Token
}
// MultiTenantServicePrincipalToken contains tokens for multi-tenant authorization.
type MultiTenantServicePrincipalToken struct {
PrimaryToken *ServicePrincipalToken
AuxiliaryTokens []*ServicePrincipalToken
}
// PrimaryOAuthToken returns the primary authorization token.
func (mt *MultiTenantServicePrincipalToken) PrimaryOAuthToken() string {
return mt.PrimaryToken.OAuthToken()
}
// AuxiliaryOAuthTokens returns one to three auxiliary authorization tokens.
func (mt *MultiTenantServicePrincipalToken) AuxiliaryOAuthTokens() []string {
tokens := make([]string, len(mt.AuxiliaryTokens))
for i := range mt.AuxiliaryTokens {
tokens[i] = mt.AuxiliaryTokens[i].OAuthToken()
}
return tokens
}
// EnsureFreshWithContext will refresh the token if it will expire within the refresh window (as set by
// RefreshWithin) and autoRefresh flag is on. This method is safe for concurrent use.
func (mt *MultiTenantServicePrincipalToken) EnsureFreshWithContext(ctx context.Context) error {
if err := mt.PrimaryToken.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh primary token: %v", err)
}
for _, aux := range mt.AuxiliaryTokens {
if err := aux.EnsureFreshWithContext(ctx); err != nil {
return fmt.Errorf("failed to refresh auxiliary token: %v", err)
}
}
return nil
}
// NewMultiTenantServicePrincipalToken creates a new MultiTenantServicePrincipalToken with the specified credentials and resource.
func NewMultiTenantServicePrincipalToken(multiTenantCfg MultiTenantOAuthConfig, clientID string, secret string, resource string) (*MultiTenantServicePrincipalToken, error) {
if err := validateStringParam(clientID, "clientID"); err != nil {
return nil, err
}
if err := validateStringParam(secret, "secret"); err != nil {
return nil, err
}
if err := validateStringParam(resource, "resource"); err != nil {
return nil, err
}
auxTenants := multiTenantCfg.AuxiliaryTenants()
m := MultiTenantServicePrincipalToken{
AuxiliaryTokens: make([]*ServicePrincipalToken, len(auxTenants)),
}
primary, err := NewServicePrincipalToken(*multiTenantCfg.PrimaryTenant(), clientID, secret, resource)
if err != nil {
return nil, fmt.Errorf("failed to create SPT for primary tenant: %v", err)
}
m.PrimaryToken = primary
for i := range auxTenants {
aux, err := NewServicePrincipalToken(*auxTenants[i], clientID, secret, resource)
if err != nil {
return nil, fmt.Errorf("failed to create SPT for auxiliary tenant: %v", err)
}
m.AuxiliaryTokens[i] = aux
}
return &m, nil
}

View File

@@ -0,0 +1,45 @@
package adal
import (
"fmt"
"runtime"
)
// Copyright 2017 Microsoft Corporation
//
// 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.
const number = "v1.0.0"
var (
ua = fmt.Sprintf("Go/%s (%s-%s) go-autorest/adal/%s",
runtime.Version(),
runtime.GOARCH,
runtime.GOOS,
number,
)
)
// UserAgent returns a string containing the Go version, system architecture and OS, and the adal version.
func UserAgent() string {
return ua
}
// AddToUserAgent adds an extension to the current user agent
func AddToUserAgent(extension string) error {
if extension != "" {
ua = fmt.Sprintf("%s %s", ua, extension)
return nil
}
return fmt.Errorf("Extension was empty, User Agent remained as '%s'", ua)
}

View File

@@ -15,12 +15,14 @@ package autorest
// limitations under the License.
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/tracing"
)
const (
@@ -30,6 +32,8 @@ const (
apiKeyAuthorizerHeader = "Ocp-Apim-Subscription-Key"
bingAPISdkHeader = "X-BingApis-SDK-Client"
golangBingAPISdkHeaderValue = "Go-SDK"
authorization = "Authorization"
basic = "Basic"
)
// Authorizer is the interface that provides a PrepareDecorator used to supply request
@@ -68,7 +72,7 @@ func NewAPIKeyAuthorizer(headers map[string]interface{}, queryParameters map[str
return &APIKeyAuthorizer{headers: headers, queryParameters: queryParameters}
}
// WithAuthorization returns a PrepareDecorator that adds an HTTP headers and Query Paramaters
// WithAuthorization returns a PrepareDecorator that adds an HTTP headers and Query Parameters.
func (aka *APIKeyAuthorizer) WithAuthorization() PrepareDecorator {
return func(p Preparer) Preparer {
return DecoratePreparer(p, WithHeaders(aka.headers), WithQueryParameters(aka.queryParameters))
@@ -113,17 +117,19 @@ func (ba *BearerAuthorizer) WithAuthorization() PrepareDecorator {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err == nil {
refresher, ok := ba.tokenProvider.(adal.Refresher)
if ok {
err := refresher.EnsureFresh()
if err != nil {
var resp *http.Response
if tokError, ok := err.(adal.TokenRefreshError); ok {
resp = tokError.Response()
}
return r, NewErrorWithError(err, "azure.BearerAuthorizer", "WithAuthorization", resp,
"Failed to refresh the Token for request to %s", r.URL)
// the ordering is important here, prefer RefresherWithContext if available
if refresher, ok := ba.tokenProvider.(adal.RefresherWithContext); ok {
err = refresher.EnsureFreshWithContext(r.Context())
} else if refresher, ok := ba.tokenProvider.(adal.Refresher); ok {
err = refresher.EnsureFresh()
}
if err != nil {
var resp *http.Response
if tokError, ok := err.(adal.TokenRefreshError); ok {
resp = tokError.Response()
}
return r, NewErrorWithError(err, "azure.BearerAuthorizer", "WithAuthorization", resp,
"Failed to refresh the Token for request to %s", r.URL)
}
return Prepare(r, WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", ba.tokenProvider.OAuthToken())))
}
@@ -145,7 +151,7 @@ type BearerAuthorizerCallback struct {
// is invoked when the HTTP request is submitted.
func NewBearerAuthorizerCallback(sender Sender, callback BearerAuthorizerCallbackFunc) *BearerAuthorizerCallback {
if sender == nil {
sender = &http.Client{}
sender = &http.Client{Transport: tracing.Transport}
}
return &BearerAuthorizerCallback{sender: sender, callback: callback}
}
@@ -255,3 +261,76 @@ func (egta EventGridKeyAuthorizer) WithAuthorization() PrepareDecorator {
}
return NewAPIKeyAuthorizerWithHeaders(headers).WithAuthorization()
}
// BasicAuthorizer implements basic HTTP authorization by adding the Authorization HTTP header
// with the value "Basic <TOKEN>" where <TOKEN> is a base64-encoded username:password tuple.
type BasicAuthorizer struct {
userName string
password string
}
// NewBasicAuthorizer creates a new BasicAuthorizer with the specified username and password.
func NewBasicAuthorizer(userName, password string) *BasicAuthorizer {
return &BasicAuthorizer{
userName: userName,
password: password,
}
}
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
// value is "Basic " followed by the base64-encoded username:password tuple.
func (ba *BasicAuthorizer) WithAuthorization() PrepareDecorator {
headers := make(map[string]interface{})
headers[authorization] = basic + " " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", ba.userName, ba.password)))
return NewAPIKeyAuthorizerWithHeaders(headers).WithAuthorization()
}
// MultiTenantServicePrincipalTokenAuthorizer provides authentication across tenants.
type MultiTenantServicePrincipalTokenAuthorizer interface {
WithAuthorization() PrepareDecorator
}
// NewMultiTenantServicePrincipalTokenAuthorizer crates a BearerAuthorizer using the given token provider
func NewMultiTenantServicePrincipalTokenAuthorizer(tp adal.MultitenantOAuthTokenProvider) MultiTenantServicePrincipalTokenAuthorizer {
return &multiTenantSPTAuthorizer{tp: tp}
}
type multiTenantSPTAuthorizer struct {
tp adal.MultitenantOAuthTokenProvider
}
// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header using the
// primary token along with the auxiliary authorization header using the auxiliary tokens.
//
// By default, the token will be automatically refreshed through the Refresher interface.
func (mt multiTenantSPTAuthorizer) WithAuthorization() PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err != nil {
return r, err
}
if refresher, ok := mt.tp.(adal.RefresherWithContext); ok {
err = refresher.EnsureFreshWithContext(r.Context())
if err != nil {
var resp *http.Response
if tokError, ok := err.(adal.TokenRefreshError); ok {
resp = tokError.Response()
}
return r, NewErrorWithError(err, "azure.multiTenantSPTAuthorizer", "WithAuthorization", resp,
"Failed to refresh one or more Tokens for request to %s", r.URL)
}
}
r, err = Prepare(r, WithHeader(headerAuthorization, fmt.Sprintf("Bearer %s", mt.tp.PrimaryOAuthToken())))
if err != nil {
return r, err
}
auxTokens := mt.tp.AuxiliaryOAuthTokens()
for i := range auxTokens {
auxTokens[i] = fmt.Sprintf("Bearer %s", auxTokens[i])
}
return Prepare(r, WithHeader(headerAuxAuthorization, strings.Join(auxTokens, "; ")))
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -31,126 +31,416 @@ import (
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/azure/cli"
"github.com/dimchansky/utfbom"
"golang.org/x/crypto/pkcs12"
)
// The possible keys in the Values map.
const (
SubscriptionID = "AZURE_SUBSCRIPTION_ID"
TenantID = "AZURE_TENANT_ID"
AuxiliaryTenantIDs = "AZURE_AUXILIARY_TENANT_IDS"
ClientID = "AZURE_CLIENT_ID"
ClientSecret = "AZURE_CLIENT_SECRET"
CertificatePath = "AZURE_CERTIFICATE_PATH"
CertificatePassword = "AZURE_CERTIFICATE_PASSWORD"
Username = "AZURE_USERNAME"
Password = "AZURE_PASSWORD"
EnvironmentName = "AZURE_ENVIRONMENT"
Resource = "AZURE_AD_RESOURCE"
ActiveDirectoryEndpoint = "ActiveDirectoryEndpoint"
ResourceManagerEndpoint = "ResourceManagerEndpoint"
GraphResourceID = "GraphResourceID"
SQLManagementEndpoint = "SQLManagementEndpoint"
GalleryEndpoint = "GalleryEndpoint"
ManagementEndpoint = "ManagementEndpoint"
)
// NewAuthorizerFromEnvironment creates an Authorizer configured from environment variables in the order:
// 1. Client credentials
// 2. Client certificate
// 3. Username password
// 4. MSI
func NewAuthorizerFromEnvironment() (autorest.Authorizer, error) {
tenantID := os.Getenv("AZURE_TENANT_ID")
clientID := os.Getenv("AZURE_CLIENT_ID")
clientSecret := os.Getenv("AZURE_CLIENT_SECRET")
certificatePath := os.Getenv("AZURE_CERTIFICATE_PATH")
certificatePassword := os.Getenv("AZURE_CERTIFICATE_PASSWORD")
username := os.Getenv("AZURE_USERNAME")
password := os.Getenv("AZURE_PASSWORD")
envName := os.Getenv("AZURE_ENVIRONMENT")
resource := os.Getenv("AZURE_AD_RESOURCE")
settings, err := GetSettingsFromEnvironment()
if err != nil {
return nil, err
}
return settings.GetAuthorizer()
}
var env azure.Environment
if envName == "" {
env = azure.PublicCloud
// NewAuthorizerFromEnvironmentWithResource creates an Authorizer configured from environment variables in the order:
// 1. Client credentials
// 2. Client certificate
// 3. Username password
// 4. MSI
func NewAuthorizerFromEnvironmentWithResource(resource string) (autorest.Authorizer, error) {
settings, err := GetSettingsFromEnvironment()
if err != nil {
return nil, err
}
settings.Values[Resource] = resource
return settings.GetAuthorizer()
}
// EnvironmentSettings contains the available authentication settings.
type EnvironmentSettings struct {
Values map[string]string
Environment azure.Environment
}
// GetSettingsFromEnvironment returns the available authentication settings from the environment.
func GetSettingsFromEnvironment() (s EnvironmentSettings, err error) {
s = EnvironmentSettings{
Values: map[string]string{},
}
s.setValue(SubscriptionID)
s.setValue(TenantID)
s.setValue(AuxiliaryTenantIDs)
s.setValue(ClientID)
s.setValue(ClientSecret)
s.setValue(CertificatePath)
s.setValue(CertificatePassword)
s.setValue(Username)
s.setValue(Password)
s.setValue(EnvironmentName)
s.setValue(Resource)
if v := s.Values[EnvironmentName]; v == "" {
s.Environment = azure.PublicCloud
} else {
var err error
env, err = azure.EnvironmentFromName(envName)
if err != nil {
return nil, err
s.Environment, err = azure.EnvironmentFromName(v)
}
if s.Values[Resource] == "" {
s.Values[Resource] = s.Environment.ResourceManagerEndpoint
}
return
}
// GetSubscriptionID returns the available subscription ID or an empty string.
func (settings EnvironmentSettings) GetSubscriptionID() string {
return settings.Values[SubscriptionID]
}
// adds the specified environment variable value to the Values map if it exists
func (settings EnvironmentSettings) setValue(key string) {
if v := os.Getenv(key); v != "" {
settings.Values[key] = v
}
}
// helper to return client and tenant IDs
func (settings EnvironmentSettings) getClientAndTenant() (string, string) {
clientID := settings.Values[ClientID]
tenantID := settings.Values[TenantID]
return clientID, tenantID
}
// GetClientCredentials creates a config object from the available client credentials.
// An error is returned if no client credentials are available.
func (settings EnvironmentSettings) GetClientCredentials() (ClientCredentialsConfig, error) {
secret := settings.Values[ClientSecret]
if secret == "" {
return ClientCredentialsConfig{}, errors.New("missing client secret")
}
clientID, tenantID := settings.getClientAndTenant()
config := NewClientCredentialsConfig(clientID, secret, tenantID)
config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
config.Resource = settings.Values[Resource]
if auxTenants, ok := settings.Values[AuxiliaryTenantIDs]; ok {
config.AuxTenants = strings.Split(auxTenants, ";")
for i := range config.AuxTenants {
config.AuxTenants[i] = strings.TrimSpace(config.AuxTenants[i])
}
}
return config, nil
}
if resource == "" {
resource = env.ResourceManagerEndpoint
// GetClientCertificate creates a config object from the available certificate credentials.
// An error is returned if no certificate credentials are available.
func (settings EnvironmentSettings) GetClientCertificate() (ClientCertificateConfig, error) {
certPath := settings.Values[CertificatePath]
if certPath == "" {
return ClientCertificateConfig{}, errors.New("missing certificate path")
}
certPwd := settings.Values[CertificatePassword]
clientID, tenantID := settings.getClientAndTenant()
config := NewClientCertificateConfig(certPath, certPwd, clientID, tenantID)
config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
config.Resource = settings.Values[Resource]
return config, nil
}
// GetUsernamePassword creates a config object from the available username/password credentials.
// An error is returned if no username/password credentials are available.
func (settings EnvironmentSettings) GetUsernamePassword() (UsernamePasswordConfig, error) {
username := settings.Values[Username]
password := settings.Values[Password]
if username == "" || password == "" {
return UsernamePasswordConfig{}, errors.New("missing username/password")
}
clientID, tenantID := settings.getClientAndTenant()
config := NewUsernamePasswordConfig(username, password, clientID, tenantID)
config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
config.Resource = settings.Values[Resource]
return config, nil
}
// GetMSI creates a MSI config object from the available client ID.
func (settings EnvironmentSettings) GetMSI() MSIConfig {
config := NewMSIConfig()
config.Resource = settings.Values[Resource]
config.ClientID = settings.Values[ClientID]
return config
}
// GetDeviceFlow creates a device-flow config object from the available client and tenant IDs.
func (settings EnvironmentSettings) GetDeviceFlow() DeviceFlowConfig {
clientID, tenantID := settings.getClientAndTenant()
config := NewDeviceFlowConfig(clientID, tenantID)
config.AADEndpoint = settings.Environment.ActiveDirectoryEndpoint
config.Resource = settings.Values[Resource]
return config
}
// GetAuthorizer creates an Authorizer configured from environment variables in the order:
// 1. Client credentials
// 2. Client certificate
// 3. Username password
// 4. MSI
func (settings EnvironmentSettings) GetAuthorizer() (autorest.Authorizer, error) {
//1.Client Credentials
if clientSecret != "" {
config := NewClientCredentialsConfig(clientID, clientSecret, tenantID)
config.AADEndpoint = env.ActiveDirectoryEndpoint
config.Resource = resource
return config.Authorizer()
if c, e := settings.GetClientCredentials(); e == nil {
return c.Authorizer()
}
//2. Client Certificate
if certificatePath != "" {
config := NewClientCertificateConfig(certificatePath, certificatePassword, clientID, tenantID)
config.AADEndpoint = env.ActiveDirectoryEndpoint
config.Resource = resource
return config.Authorizer()
if c, e := settings.GetClientCertificate(); e == nil {
return c.Authorizer()
}
//3. Username Password
if username != "" && password != "" {
config := NewUsernamePasswordConfig(username, password, clientID, tenantID)
config.AADEndpoint = env.ActiveDirectoryEndpoint
config.Resource = resource
return config.Authorizer()
if c, e := settings.GetUsernamePassword(); e == nil {
return c.Authorizer()
}
// 4. MSI
config := NewMSIConfig()
config.Resource = resource
config.ClientID = clientID
return config.Authorizer()
return settings.GetMSI().Authorizer()
}
// NewAuthorizerFromFile creates an Authorizer configured from a configuration file.
// NewAuthorizerFromFile creates an Authorizer configured from a configuration file in the following order.
// 1. Client credentials
// 2. Client certificate
func NewAuthorizerFromFile(baseURI string) (autorest.Authorizer, error) {
settings, err := GetSettingsFromFile()
if err != nil {
return nil, err
}
if a, err := settings.ClientCredentialsAuthorizer(baseURI); err == nil {
return a, err
}
if a, err := settings.ClientCertificateAuthorizer(baseURI); err == nil {
return a, err
}
return nil, errors.New("auth file missing client and certificate credentials")
}
// NewAuthorizerFromFileWithResource creates an Authorizer configured from a configuration file in the following order.
// 1. Client credentials
// 2. Client certificate
func NewAuthorizerFromFileWithResource(resource string) (autorest.Authorizer, error) {
s, err := GetSettingsFromFile()
if err != nil {
return nil, err
}
if a, err := s.ClientCredentialsAuthorizerWithResource(resource); err == nil {
return a, err
}
if a, err := s.ClientCertificateAuthorizerWithResource(resource); err == nil {
return a, err
}
return nil, errors.New("auth file missing client and certificate credentials")
}
// NewAuthorizerFromCLI creates an Authorizer configured from Azure CLI 2.0 for local development scenarios.
func NewAuthorizerFromCLI() (autorest.Authorizer, error) {
settings, err := GetSettingsFromEnvironment()
if err != nil {
return nil, err
}
if settings.Values[Resource] == "" {
settings.Values[Resource] = settings.Environment.ResourceManagerEndpoint
}
return NewAuthorizerFromCLIWithResource(settings.Values[Resource])
}
// NewAuthorizerFromCLIWithResource creates an Authorizer configured from Azure CLI 2.0 for local development scenarios.
func NewAuthorizerFromCLIWithResource(resource string) (autorest.Authorizer, error) {
token, err := cli.GetTokenFromCLI(resource)
if err != nil {
return nil, err
}
adalToken, err := token.ToADALToken()
if err != nil {
return nil, err
}
return autorest.NewBearerAuthorizer(&adalToken), nil
}
// GetSettingsFromFile returns the available authentication settings from an Azure CLI authentication file.
func GetSettingsFromFile() (FileSettings, error) {
s := FileSettings{}
fileLocation := os.Getenv("AZURE_AUTH_LOCATION")
if fileLocation == "" {
return nil, errors.New("auth file not found. Environment variable AZURE_AUTH_LOCATION is not set")
return s, errors.New("environment variable AZURE_AUTH_LOCATION is not set")
}
contents, err := ioutil.ReadFile(fileLocation)
if err != nil {
return nil, err
return s, err
}
// Auth file might be encoded
decoded, err := decode(contents)
if err != nil {
return nil, err
return s, err
}
file := file{}
err = json.Unmarshal(decoded, &file)
authFile := map[string]interface{}{}
err = json.Unmarshal(decoded, &authFile)
if err != nil {
return s, err
}
s.Values = map[string]string{}
s.setKeyValue(ClientID, authFile["clientId"])
s.setKeyValue(ClientSecret, authFile["clientSecret"])
s.setKeyValue(CertificatePath, authFile["clientCertificate"])
s.setKeyValue(CertificatePassword, authFile["clientCertificatePassword"])
s.setKeyValue(SubscriptionID, authFile["subscriptionId"])
s.setKeyValue(TenantID, authFile["tenantId"])
s.setKeyValue(ActiveDirectoryEndpoint, authFile["activeDirectoryEndpointUrl"])
s.setKeyValue(ResourceManagerEndpoint, authFile["resourceManagerEndpointUrl"])
s.setKeyValue(GraphResourceID, authFile["activeDirectoryGraphResourceId"])
s.setKeyValue(SQLManagementEndpoint, authFile["sqlManagementEndpointUrl"])
s.setKeyValue(GalleryEndpoint, authFile["galleryEndpointUrl"])
s.setKeyValue(ManagementEndpoint, authFile["managementEndpointUrl"])
return s, nil
}
// FileSettings contains the available authentication settings.
type FileSettings struct {
Values map[string]string
}
// GetSubscriptionID returns the available subscription ID or an empty string.
func (settings FileSettings) GetSubscriptionID() string {
return settings.Values[SubscriptionID]
}
// adds the specified value to the Values map if it isn't nil
func (settings FileSettings) setKeyValue(key string, val interface{}) {
if val != nil {
settings.Values[key] = val.(string)
}
}
// returns the specified AAD endpoint or the public cloud endpoint if unspecified
func (settings FileSettings) getAADEndpoint() string {
if v, ok := settings.Values[ActiveDirectoryEndpoint]; ok {
return v
}
return azure.PublicCloud.ActiveDirectoryEndpoint
}
// ServicePrincipalTokenFromClientCredentials creates a ServicePrincipalToken from the available client credentials.
func (settings FileSettings) ServicePrincipalTokenFromClientCredentials(baseURI string) (*adal.ServicePrincipalToken, error) {
resource, err := settings.getResourceForToken(baseURI)
if err != nil {
return nil, err
}
return settings.ServicePrincipalTokenFromClientCredentialsWithResource(resource)
}
resource, err := getResourceForToken(file, baseURI)
// ClientCredentialsAuthorizer creates an authorizer from the available client credentials.
func (settings FileSettings) ClientCredentialsAuthorizer(baseURI string) (autorest.Authorizer, error) {
resource, err := settings.getResourceForToken(baseURI)
if err != nil {
return nil, err
}
return settings.ClientCredentialsAuthorizerWithResource(resource)
}
config, err := adal.NewOAuthConfig(file.ActiveDirectoryEndpoint, file.TenantID)
// ServicePrincipalTokenFromClientCredentialsWithResource creates a ServicePrincipalToken
// from the available client credentials and the specified resource.
func (settings FileSettings) ServicePrincipalTokenFromClientCredentialsWithResource(resource string) (*adal.ServicePrincipalToken, error) {
if _, ok := settings.Values[ClientSecret]; !ok {
return nil, errors.New("missing client secret")
}
config, err := adal.NewOAuthConfig(settings.getAADEndpoint(), settings.Values[TenantID])
if err != nil {
return nil, err
}
return adal.NewServicePrincipalToken(*config, settings.Values[ClientID], settings.Values[ClientSecret], resource)
}
spToken, err := adal.NewServicePrincipalToken(*config, file.ClientID, file.ClientSecret, resource)
func (settings FileSettings) clientCertificateConfigWithResource(resource string) (ClientCertificateConfig, error) {
if _, ok := settings.Values[CertificatePath]; !ok {
return ClientCertificateConfig{}, errors.New("missing certificate path")
}
cfg := NewClientCertificateConfig(settings.Values[CertificatePath], settings.Values[CertificatePassword], settings.Values[ClientID], settings.Values[TenantID])
cfg.AADEndpoint = settings.getAADEndpoint()
cfg.Resource = resource
return cfg, nil
}
// ClientCredentialsAuthorizerWithResource creates an authorizer from the available client credentials and the specified resource.
func (settings FileSettings) ClientCredentialsAuthorizerWithResource(resource string) (autorest.Authorizer, error) {
spToken, err := settings.ServicePrincipalTokenFromClientCredentialsWithResource(resource)
if err != nil {
return nil, err
}
return autorest.NewBearerAuthorizer(spToken), nil
}
// File represents the authentication file
type file struct {
ClientID string `json:"clientId,omitempty"`
ClientSecret string `json:"clientSecret,omitempty"`
SubscriptionID string `json:"subscriptionId,omitempty"`
TenantID string `json:"tenantId,omitempty"`
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpointUrl,omitempty"`
ResourceManagerEndpoint string `json:"resourceManagerEndpointUrl,omitempty"`
GraphResourceID string `json:"activeDirectoryGraphResourceId,omitempty"`
SQLManagementEndpoint string `json:"sqlManagementEndpointUrl,omitempty"`
GalleryEndpoint string `json:"galleryEndpointUrl,omitempty"`
ManagementEndpoint string `json:"managementEndpointUrl,omitempty"`
// ServicePrincipalTokenFromClientCertificate creates a ServicePrincipalToken from the available certificate credentials.
func (settings FileSettings) ServicePrincipalTokenFromClientCertificate(baseURI string) (*adal.ServicePrincipalToken, error) {
resource, err := settings.getResourceForToken(baseURI)
if err != nil {
return nil, err
}
return settings.ServicePrincipalTokenFromClientCertificateWithResource(resource)
}
// ClientCertificateAuthorizer creates an authorizer from the available certificate credentials.
func (settings FileSettings) ClientCertificateAuthorizer(baseURI string) (autorest.Authorizer, error) {
resource, err := settings.getResourceForToken(baseURI)
if err != nil {
return nil, err
}
return settings.ClientCertificateAuthorizerWithResource(resource)
}
// ServicePrincipalTokenFromClientCertificateWithResource creates a ServicePrincipalToken from the available certificate credentials.
func (settings FileSettings) ServicePrincipalTokenFromClientCertificateWithResource(resource string) (*adal.ServicePrincipalToken, error) {
cfg, err := settings.clientCertificateConfigWithResource(resource)
if err != nil {
return nil, err
}
return cfg.ServicePrincipalToken()
}
// ClientCertificateAuthorizerWithResource creates an authorizer from the available certificate credentials and the specified resource.
func (settings FileSettings) ClientCertificateAuthorizerWithResource(resource string) (autorest.Authorizer, error) {
cfg, err := settings.clientCertificateConfigWithResource(resource)
if err != nil {
return nil, err
}
return cfg.Authorizer()
}
func decode(b []byte) ([]byte, error) {
@@ -175,7 +465,7 @@ func decode(b []byte) ([]byte, error) {
return ioutil.ReadAll(reader)
}
func getResourceForToken(f file, baseURI string) (string, error) {
func (settings FileSettings) getResourceForToken(baseURI string) (string, error) {
// Compare dafault base URI from the SDK to the endpoints from the public cloud
// Base URI and token resource are the same string. This func finds the authentication
// file field that matches the SDK base URI. The SDK defines the public cloud
@@ -185,15 +475,15 @@ func getResourceForToken(f file, baseURI string) (string, error) {
}
switch baseURI {
case azure.PublicCloud.ServiceManagementEndpoint:
return f.ManagementEndpoint, nil
return settings.Values[ManagementEndpoint], nil
case azure.PublicCloud.ResourceManagerEndpoint:
return f.ResourceManagerEndpoint, nil
return settings.Values[ResourceManagerEndpoint], nil
case azure.PublicCloud.ActiveDirectoryEndpoint:
return f.ActiveDirectoryEndpoint, nil
return settings.Values[ActiveDirectoryEndpoint], nil
case azure.PublicCloud.GalleryEndpoint:
return f.GalleryEndpoint, nil
return settings.Values[GalleryEndpoint], nil
case azure.PublicCloud.GraphEndpoint:
return f.GraphResourceID, nil
return settings.Values[GraphResourceID], nil
}
return "", fmt.Errorf("auth: base URI not found in endpoints")
}
@@ -264,23 +554,43 @@ type ClientCredentialsConfig struct {
ClientID string
ClientSecret string
TenantID string
AuxTenants []string
AADEndpoint string
Resource string
}
// Authorizer gets the authorizer from client credentials.
func (ccc ClientCredentialsConfig) Authorizer() (autorest.Authorizer, error) {
// ServicePrincipalToken creates a ServicePrincipalToken from client credentials.
func (ccc ClientCredentialsConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(ccc.AADEndpoint, ccc.TenantID)
if err != nil {
return nil, err
}
return adal.NewServicePrincipalToken(*oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource)
}
spToken, err := adal.NewServicePrincipalToken(*oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource)
// MultiTenantServicePrincipalToken creates a MultiTenantServicePrincipalToken from client credentials.
func (ccc ClientCredentialsConfig) MultiTenantServicePrincipalToken() (*adal.MultiTenantServicePrincipalToken, error) {
oauthConfig, err := adal.NewMultiTenantOAuthConfig(ccc.AADEndpoint, ccc.TenantID, ccc.AuxTenants, adal.OAuthOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from client credentials: %v", err)
return nil, err
}
return adal.NewMultiTenantServicePrincipalToken(oauthConfig, ccc.ClientID, ccc.ClientSecret, ccc.Resource)
}
return autorest.NewBearerAuthorizer(spToken), nil
// Authorizer gets the authorizer from client credentials.
func (ccc ClientCredentialsConfig) Authorizer() (autorest.Authorizer, error) {
if len(ccc.AuxTenants) == 0 {
spToken, err := ccc.ServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get SPT from client credentials: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
mtSPT, err := ccc.MultiTenantServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get multitenant SPT from client credentials: %v", err)
}
return autorest.NewMultiTenantServicePrincipalTokenAuthorizer(mtSPT), nil
}
// ClientCertificateConfig provides the options to get a bearer authorizer from a client certificate.
@@ -293,26 +603,29 @@ type ClientCertificateConfig struct {
Resource string
}
// Authorizer gets an authorizer object from client certificate.
func (ccc ClientCertificateConfig) Authorizer() (autorest.Authorizer, error) {
// ServicePrincipalToken creates a ServicePrincipalToken from client certificate.
func (ccc ClientCertificateConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(ccc.AADEndpoint, ccc.TenantID)
if err != nil {
return nil, err
}
certData, err := ioutil.ReadFile(ccc.CertificatePath)
if err != nil {
return nil, fmt.Errorf("failed to read the certificate file (%s): %v", ccc.CertificatePath, err)
}
certificate, rsaPrivateKey, err := decodePkcs12(certData, ccc.CertificatePassword)
if err != nil {
return nil, fmt.Errorf("failed to decode pkcs12 certificate while creating spt: %v", err)
}
return adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
}
spToken, err := adal.NewServicePrincipalTokenFromCertificate(*oauthConfig, ccc.ClientID, certificate, rsaPrivateKey, ccc.Resource)
// Authorizer gets an authorizer object from client certificate.
func (ccc ClientCertificateConfig) Authorizer() (autorest.Authorizer, error) {
spToken, err := ccc.ServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from certificate auth: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
@@ -326,26 +639,30 @@ type DeviceFlowConfig struct {
// Authorizer gets the authorizer from device flow.
func (dfc DeviceFlowConfig) Authorizer() (autorest.Authorizer, error) {
oauthClient := &autorest.Client{}
spToken, err := dfc.ServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from device flow: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
// ServicePrincipalToken gets the service principal token from device flow.
func (dfc DeviceFlowConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(dfc.AADEndpoint, dfc.TenantID)
deviceCode, err := adal.InitiateDeviceAuth(oauthClient, *oauthConfig, dfc.ClientID, dfc.AADEndpoint)
if err != nil {
return nil, err
}
oauthClient := &autorest.Client{}
deviceCode, err := adal.InitiateDeviceAuth(oauthClient, *oauthConfig, dfc.ClientID, dfc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to start device auth flow: %s", err)
}
log.Println(*deviceCode.Message)
token, err := adal.WaitForUserCompletion(oauthClient, deviceCode)
if err != nil {
return nil, fmt.Errorf("failed to finish device auth flow: %s", err)
}
spToken, err := adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, dfc.ClientID, dfc.Resource, *token)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from device flow: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
return adal.NewServicePrincipalTokenFromManualToken(*oauthConfig, dfc.ClientID, dfc.Resource, *token)
}
func decodePkcs12(pkcs []byte, password string) (*x509.Certificate, *rsa.PrivateKey, error) {
@@ -372,17 +689,21 @@ type UsernamePasswordConfig struct {
Resource string
}
// ServicePrincipalToken creates a ServicePrincipalToken from username and password.
func (ups UsernamePasswordConfig) ServicePrincipalToken() (*adal.ServicePrincipalToken, error) {
oauthConfig, err := adal.NewOAuthConfig(ups.AADEndpoint, ups.TenantID)
if err != nil {
return nil, err
}
return adal.NewServicePrincipalTokenFromUsernamePassword(*oauthConfig, ups.ClientID, ups.Username, ups.Password, ups.Resource)
}
// Authorizer gets the authorizer from a username and a password.
func (ups UsernamePasswordConfig) Authorizer() (autorest.Authorizer, error) {
oauthConfig, err := adal.NewOAuthConfig(ups.AADEndpoint, ups.TenantID)
spToken, err := adal.NewServicePrincipalTokenFromUsernamePassword(*oauthConfig, ups.ClientID, ups.Username, ups.Password, ups.Resource)
spToken, err := ups.ServicePrincipalToken()
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from username and password auth: %v", err)
}
return autorest.NewBearerAuthorizer(spToken), nil
}
@@ -399,9 +720,17 @@ func (mc MSIConfig) Authorizer() (autorest.Authorizer, error) {
return nil, err
}
spToken, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
var spToken *adal.ServicePrincipalToken
if mc.ClientID == "" {
spToken, err = adal.NewServicePrincipalTokenFromMSI(msiEndpoint, mc.Resource)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI: %v", err)
}
} else {
spToken, err = adal.NewServicePrincipalTokenFromMSIWithUserAssignedID(msiEndpoint, mc.Resource, mc.ClientID)
if err != nil {
return nil, fmt.Errorf("failed to get oauth token from MSI for user assigned identity: %v", err)
}
}
return autorest.NewBearerAuthorizer(spToken), nil

View File

@@ -44,11 +44,12 @@ const (
// ServiceError encapsulates the error response from an Azure service.
// It adhears to the OData v4 specification for error responses.
type ServiceError struct {
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details []map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details []map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
AdditionalInfo []map[string]interface{} `json:"additionalInfo"`
}
func (se ServiceError) Error() string {
@@ -74,6 +75,14 @@ func (se ServiceError) Error() string {
result += fmt.Sprintf(" InnerError=%v", string(d))
}
if se.AdditionalInfo != nil {
d, err := json.Marshal(se.AdditionalInfo)
if err != nil {
result += fmt.Sprintf(" AdditionalInfo=%v", se.AdditionalInfo)
}
result += fmt.Sprintf(" AdditionalInfo=%v", string(d))
}
return result
}
@@ -86,44 +95,47 @@ func (se *ServiceError) UnmarshalJSON(b []byte) error {
// http://docs.oasis-open.org/odata/odata-json-format/v4.0/os/odata-json-format-v4.0-os.html#_Toc372793091
type serviceError1 struct {
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details []map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details []map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
AdditionalInfo []map[string]interface{} `json:"additionalInfo"`
}
type serviceError2 struct {
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
Code string `json:"code"`
Message string `json:"message"`
Target *string `json:"target"`
Details map[string]interface{} `json:"details"`
InnerError map[string]interface{} `json:"innererror"`
AdditionalInfo []map[string]interface{} `json:"additionalInfo"`
}
se1 := serviceError1{}
err := json.Unmarshal(b, &se1)
if err == nil {
se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError)
se.populate(se1.Code, se1.Message, se1.Target, se1.Details, se1.InnerError, se1.AdditionalInfo)
return nil
}
se2 := serviceError2{}
err = json.Unmarshal(b, &se2)
if err == nil {
se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError)
se.populate(se2.Code, se2.Message, se2.Target, nil, se2.InnerError, se2.AdditionalInfo)
se.Details = append(se.Details, se2.Details)
return nil
}
return err
}
func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}) {
func (se *ServiceError) populate(code, message string, target *string, details []map[string]interface{}, inner map[string]interface{}, additional []map[string]interface{}) {
se.Code = code
se.Message = message
se.Target = target
se.Details = details
se.InnerError = inner
se.AdditionalInfo = additional
}
// RequestError describes an error response returned by Azure service.
@@ -279,16 +291,29 @@ func WithErrorUnlessStatusCode(codes ...int) autorest.RespondDecorator {
resp.Body = ioutil.NopCloser(&b)
if decodeErr != nil {
return fmt.Errorf("autorest/azure: error response cannot be parsed: %q error: %v", b.String(), decodeErr)
} else if e.ServiceError == nil {
}
if e.ServiceError == nil {
// Check if error is unwrapped ServiceError
if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil || e.ServiceError.Message == "" {
e.ServiceError = &ServiceError{
Code: "Unknown",
Message: "Unknown service error",
}
if err := json.Unmarshal(b.Bytes(), &e.ServiceError); err != nil {
return err
}
}
if e.ServiceError.Message == "" {
// if we're here it means the returned error wasn't OData v4 compliant.
// try to unmarshal the body as raw JSON in hopes of getting something.
rawBody := map[string]interface{}{}
if err := json.Unmarshal(b.Bytes(), &rawBody); err != nil {
return err
}
e.ServiceError = &ServiceError{
Code: "Unknown",
Message: "Unknown service error",
}
if len(rawBody) > 0 {
e.ServiceError.Details = []map[string]interface{}{rawBody}
}
}
e.Response = resp
e.RequestID = ExtractRequestID(resp)
if e.StatusCode == nil {
e.StatusCode = resp.StatusCode

View File

@@ -0,0 +1,79 @@
package cli
// Copyright 2017 Microsoft Corporation
//
// 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.
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/dimchansky/utfbom"
"github.com/mitchellh/go-homedir"
)
// Profile represents a Profile from the Azure CLI
type Profile struct {
InstallationID string `json:"installationId"`
Subscriptions []Subscription `json:"subscriptions"`
}
// Subscription represents a Subscription from the Azure CLI
type Subscription struct {
EnvironmentName string `json:"environmentName"`
ID string `json:"id"`
IsDefault bool `json:"isDefault"`
Name string `json:"name"`
State string `json:"state"`
TenantID string `json:"tenantId"`
User *User `json:"user"`
}
// User represents a User from the Azure CLI
type User struct {
Name string `json:"name"`
Type string `json:"type"`
}
const azureProfileJSON = "azureProfile.json"
// ProfilePath returns the path where the Azure Profile is stored from the Azure CLI
func ProfilePath() (string, error) {
if cfgDir := os.Getenv("AZURE_CONFIG_DIR"); cfgDir != "" {
return filepath.Join(cfgDir, azureProfileJSON), nil
}
return homedir.Expand("~/.azure/" + azureProfileJSON)
}
// LoadProfile restores a Profile object from a file located at 'path'.
func LoadProfile(path string) (result Profile, err error) {
var contents []byte
contents, err = ioutil.ReadFile(path)
if err != nil {
err = fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
return
}
reader := utfbom.SkipOnly(bytes.NewReader(contents))
dec := json.NewDecoder(reader)
if err = dec.Decode(&result); err != nil {
err = fmt.Errorf("failed to decode contents of file (%s) into a Profile representation: %v", path, err)
return
}
return
}

View File

@@ -0,0 +1,170 @@
package cli
// Copyright 2017 Microsoft Corporation
//
// 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.
import (
"bytes"
"encoding/json"
"fmt"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"time"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/date"
"github.com/mitchellh/go-homedir"
)
// Token represents an AccessToken from the Azure CLI
type Token struct {
AccessToken string `json:"accessToken"`
Authority string `json:"_authority"`
ClientID string `json:"_clientId"`
ExpiresOn string `json:"expiresOn"`
IdentityProvider string `json:"identityProvider"`
IsMRRT bool `json:"isMRRT"`
RefreshToken string `json:"refreshToken"`
Resource string `json:"resource"`
TokenType string `json:"tokenType"`
UserID string `json:"userId"`
}
// ToADALToken converts an Azure CLI `Token`` to an `adal.Token``
func (t Token) ToADALToken() (converted adal.Token, err error) {
tokenExpirationDate, err := ParseExpirationDate(t.ExpiresOn)
if err != nil {
err = fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err)
return
}
difference := tokenExpirationDate.Sub(date.UnixEpoch())
converted = adal.Token{
AccessToken: t.AccessToken,
Type: t.TokenType,
ExpiresIn: "3600",
ExpiresOn: json.Number(strconv.Itoa(int(difference.Seconds()))),
RefreshToken: t.RefreshToken,
Resource: t.Resource,
}
return
}
// AccessTokensPath returns the path where access tokens are stored from the Azure CLI
// TODO(#199): add unit test.
func AccessTokensPath() (string, error) {
// Azure-CLI allows user to customize the path of access tokens thorugh environment variable.
var accessTokenPath = os.Getenv("AZURE_ACCESS_TOKEN_FILE")
var err error
// Fallback logic to default path on non-cloud-shell environment.
// TODO(#200): remove the dependency on hard-coding path.
if accessTokenPath == "" {
accessTokenPath, err = homedir.Expand("~/.azure/accessTokens.json")
}
return accessTokenPath, err
}
// ParseExpirationDate parses either a Azure CLI or CloudShell date into a time object
func ParseExpirationDate(input string) (*time.Time, error) {
// CloudShell (and potentially the Azure CLI in future)
expirationDate, cloudShellErr := time.Parse(time.RFC3339, input)
if cloudShellErr != nil {
// Azure CLI (Python) e.g. 2017-08-31 19:48:57.998857 (plus the local timezone)
const cliFormat = "2006-01-02 15:04:05.999999"
expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local)
if cliErr == nil {
return &expirationDate, nil
}
return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr)
}
return &expirationDate, nil
}
// LoadTokens restores a set of Token objects from a file located at 'path'.
func LoadTokens(path string) ([]Token, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open file (%s) while loading token: %v", path, err)
}
defer file.Close()
var tokens []Token
dec := json.NewDecoder(file)
if err = dec.Decode(&tokens); err != nil {
return nil, fmt.Errorf("failed to decode contents of file (%s) into a `cli.Token` representation: %v", path, err)
}
return tokens, nil
}
// GetTokenFromCLI gets a token using Azure CLI 2.0 for local development scenarios.
func GetTokenFromCLI(resource string) (*Token, error) {
// This is the path that a developer can set to tell this class what the install path for Azure CLI is.
const azureCLIPath = "AzureCLIPath"
// The default install paths are used to find Azure CLI. This is for security, so that any path in the calling program's Path environment is not used to execute Azure CLI.
azureCLIDefaultPathWindows := fmt.Sprintf("%s\\Microsoft SDKs\\Azure\\CLI2\\wbin; %s\\Microsoft SDKs\\Azure\\CLI2\\wbin", os.Getenv("ProgramFiles(x86)"), os.Getenv("ProgramFiles"))
// Default path for non-Windows.
const azureCLIDefaultPath = "/bin:/sbin:/usr/bin:/usr/local/bin"
// Validate resource, since it gets sent as a command line argument to Azure CLI
const invalidResourceErrorTemplate = "Resource %s is not in expected format. Only alphanumeric characters, [dot], [colon], [hyphen], and [forward slash] are allowed."
match, err := regexp.MatchString("^[0-9a-zA-Z-.:/]+$", resource)
if err != nil {
return nil, err
}
if !match {
return nil, fmt.Errorf(invalidResourceErrorTemplate, resource)
}
// Execute Azure CLI to get token
var cliCmd *exec.Cmd
if runtime.GOOS == "windows" {
cliCmd = exec.Command(fmt.Sprintf("%s\\system32\\cmd.exe", os.Getenv("windir")))
cliCmd.Env = os.Environ()
cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s;%s", os.Getenv(azureCLIPath), azureCLIDefaultPathWindows))
cliCmd.Args = append(cliCmd.Args, "/c", "az")
} else {
cliCmd = exec.Command("az")
cliCmd.Env = os.Environ()
cliCmd.Env = append(cliCmd.Env, fmt.Sprintf("PATH=%s:%s", os.Getenv(azureCLIPath), azureCLIDefaultPath))
}
cliCmd.Args = append(cliCmd.Args, "account", "get-access-token", "-o", "json", "--resource", resource)
var stderr bytes.Buffer
cliCmd.Stderr = &stderr
output, err := cliCmd.Output()
if err != nil {
return nil, fmt.Errorf("Invoking Azure CLI failed with the following error: %s", stderr.String())
}
tokenResponse := Token{}
err = json.Unmarshal(output, &tokenResponse)
if err != nil {
return nil, err
}
return &tokenResponse, err
}

View File

@@ -22,9 +22,14 @@ import (
"strings"
)
// EnvironmentFilepathName captures the name of the environment variable containing the path to the file
// to be used while populating the Azure Environment.
const EnvironmentFilepathName = "AZURE_ENVIRONMENT_FILEPATH"
const (
// EnvironmentFilepathName captures the name of the environment variable containing the path to the file
// to be used while populating the Azure Environment.
EnvironmentFilepathName = "AZURE_ENVIRONMENT_FILEPATH"
// NotAvailable is used for endpoints and resource IDs that are not available for a given cloud.
NotAvailable = "N/A"
)
var environments = map[string]Environment{
"AZURECHINACLOUD": ChinaCloud,
@@ -33,28 +38,40 @@ var environments = map[string]Environment{
"AZUREUSGOVERNMENTCLOUD": USGovernmentCloud,
}
// ResourceIdentifier contains a set of Azure resource IDs.
type ResourceIdentifier struct {
Graph string `json:"graph"`
KeyVault string `json:"keyVault"`
Datalake string `json:"datalake"`
Batch string `json:"batch"`
OperationalInsights string `json:"operationalInsights"`
Storage string `json:"storage"`
}
// Environment represents a set of endpoints for each of Azure's Clouds.
type Environment struct {
Name string `json:"name"`
ManagementPortalURL string `json:"managementPortalURL"`
PublishSettingsURL string `json:"publishSettingsURL"`
ServiceManagementEndpoint string `json:"serviceManagementEndpoint"`
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
GalleryEndpoint string `json:"galleryEndpoint"`
KeyVaultEndpoint string `json:"keyVaultEndpoint"`
GraphEndpoint string `json:"graphEndpoint"`
ServiceBusEndpoint string `json:"serviceBusEndpoint"`
BatchManagementEndpoint string `json:"batchManagementEndpoint"`
StorageEndpointSuffix string `json:"storageEndpointSuffix"`
SQLDatabaseDNSSuffix string `json:"sqlDatabaseDNSSuffix"`
TrafficManagerDNSSuffix string `json:"trafficManagerDNSSuffix"`
KeyVaultDNSSuffix string `json:"keyVaultDNSSuffix"`
ServiceBusEndpointSuffix string `json:"serviceBusEndpointSuffix"`
ServiceManagementVMDNSSuffix string `json:"serviceManagementVMDNSSuffix"`
ResourceManagerVMDNSSuffix string `json:"resourceManagerVMDNSSuffix"`
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix"`
TokenAudience string `json:"tokenAudience"`
Name string `json:"name"`
ManagementPortalURL string `json:"managementPortalURL"`
PublishSettingsURL string `json:"publishSettingsURL"`
ServiceManagementEndpoint string `json:"serviceManagementEndpoint"`
ResourceManagerEndpoint string `json:"resourceManagerEndpoint"`
ActiveDirectoryEndpoint string `json:"activeDirectoryEndpoint"`
GalleryEndpoint string `json:"galleryEndpoint"`
KeyVaultEndpoint string `json:"keyVaultEndpoint"`
GraphEndpoint string `json:"graphEndpoint"`
ServiceBusEndpoint string `json:"serviceBusEndpoint"`
BatchManagementEndpoint string `json:"batchManagementEndpoint"`
StorageEndpointSuffix string `json:"storageEndpointSuffix"`
SQLDatabaseDNSSuffix string `json:"sqlDatabaseDNSSuffix"`
TrafficManagerDNSSuffix string `json:"trafficManagerDNSSuffix"`
KeyVaultDNSSuffix string `json:"keyVaultDNSSuffix"`
ServiceBusEndpointSuffix string `json:"serviceBusEndpointSuffix"`
ServiceManagementVMDNSSuffix string `json:"serviceManagementVMDNSSuffix"`
ResourceManagerVMDNSSuffix string `json:"resourceManagerVMDNSSuffix"`
ContainerRegistryDNSSuffix string `json:"containerRegistryDNSSuffix"`
CosmosDBDNSSuffix string `json:"cosmosDBDNSSuffix"`
TokenAudience string `json:"tokenAudience"`
ResourceIdentifiers ResourceIdentifier `json:"resourceIdentifiers"`
}
var (
@@ -79,7 +96,16 @@ var (
ServiceManagementVMDNSSuffix: "cloudapp.net",
ResourceManagerVMDNSSuffix: "cloudapp.azure.com",
ContainerRegistryDNSSuffix: "azurecr.io",
CosmosDBDNSSuffix: "documents.azure.com",
TokenAudience: "https://management.azure.com/",
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.windows.net/",
KeyVault: "https://vault.azure.net",
Datalake: "https://datalake.azure.net/",
Batch: "https://batch.core.windows.net/",
OperationalInsights: "https://api.loganalytics.io",
Storage: "https://storage.azure.com/",
},
}
// USGovernmentCloud is the cloud environment for the US Government
@@ -102,8 +128,17 @@ var (
ServiceBusEndpointSuffix: "servicebus.usgovcloudapi.net",
ServiceManagementVMDNSSuffix: "usgovcloudapp.net",
ResourceManagerVMDNSSuffix: "cloudapp.windowsazure.us",
ContainerRegistryDNSSuffix: "azurecr.io",
ContainerRegistryDNSSuffix: "azurecr.us",
CosmosDBDNSSuffix: "documents.azure.us",
TokenAudience: "https://management.usgovcloudapi.net/",
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.windows.net/",
KeyVault: "https://vault.usgovcloudapi.net",
Datalake: NotAvailable,
Batch: "https://batch.core.usgovcloudapi.net/",
OperationalInsights: "https://api.loganalytics.us",
Storage: "https://storage.azure.com/",
},
}
// ChinaCloud is the cloud environment operated in China
@@ -126,8 +161,17 @@ var (
ServiceBusEndpointSuffix: "servicebus.chinacloudapi.cn",
ServiceManagementVMDNSSuffix: "chinacloudapp.cn",
ResourceManagerVMDNSSuffix: "cloudapp.azure.cn",
ContainerRegistryDNSSuffix: "azurecr.io",
ContainerRegistryDNSSuffix: "azurecr.cn",
CosmosDBDNSSuffix: "documents.azure.cn",
TokenAudience: "https://management.chinacloudapi.cn/",
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.chinacloudapi.cn/",
KeyVault: "https://vault.azure.cn",
Datalake: NotAvailable,
Batch: "https://batch.chinacloudapi.cn/",
OperationalInsights: NotAvailable,
Storage: "https://storage.azure.com/",
},
}
// GermanCloud is the cloud environment operated in Germany
@@ -150,8 +194,17 @@ var (
ServiceBusEndpointSuffix: "servicebus.cloudapi.de",
ServiceManagementVMDNSSuffix: "azurecloudapp.de",
ResourceManagerVMDNSSuffix: "cloudapp.microsoftazure.de",
ContainerRegistryDNSSuffix: "azurecr.io",
ContainerRegistryDNSSuffix: NotAvailable,
CosmosDBDNSSuffix: "documents.microsoftazure.de",
TokenAudience: "https://management.microsoftazure.de/",
ResourceIdentifiers: ResourceIdentifier{
Graph: "https://graph.cloudapi.de/",
KeyVault: "https://vault.microsoftazure.de",
Datalake: NotAvailable,
Batch: "https://batch.cloudapi.de/",
OperationalInsights: NotAvailable,
Storage: "https://storage.azure.com/",
},
}
)

View File

@@ -64,7 +64,7 @@ func DoRetryWithRegistration(client autorest.Client) autorest.SendDecorator {
}
}
}
return resp, fmt.Errorf("failed request: %s", err)
return resp, err
})
}
}
@@ -140,8 +140,8 @@ func register(client autorest.Client, originalReq *http.Request, re RequestError
}
// poll for registered provisioning state
now := time.Now()
for err == nil && time.Since(now) < client.PollingDuration {
registrationStartTime := time.Now()
for err == nil && (client.PollingDuration == 0 || (client.PollingDuration != 0 && time.Since(registrationStartTime) < client.PollingDuration)) {
// taken from the resources SDK
// https://github.com/Azure/azure-sdk-for-go/blob/9f366792afa3e0ddaecdc860e793ba9d75e76c27/arm/resources/resources/providers.go#L45
preparer := autorest.CreatePreparer(
@@ -183,7 +183,7 @@ func register(client autorest.Client, originalReq *http.Request, re RequestError
return originalReq.Context().Err()
}
}
if !(time.Since(now) < client.PollingDuration) {
if client.PollingDuration != 0 && !(time.Since(registrationStartTime) < client.PollingDuration) {
return errors.New("polling for resource provider registration has exceeded the polling duration")
}
return err

View File

@@ -16,14 +16,18 @@ package autorest
import (
"bytes"
"crypto/tls"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/http/cookiejar"
"runtime"
"strings"
"time"
"github.com/Azure/go-autorest/logger"
"github.com/Azure/go-autorest/tracing"
)
const (
@@ -41,15 +45,6 @@ const (
)
var (
// defaultUserAgent builds a string containing the Go version, system archityecture and OS,
// and the go-autorest version.
defaultUserAgent = fmt.Sprintf("Go/%s (%s-%s) go-autorest/%s",
runtime.Version(),
runtime.GOARCH,
runtime.GOOS,
Version(),
)
// StatusCodesForRetry are a defined group of status code for which the client will retry
StatusCodesForRetry = []int{
http.StatusRequestTimeout, // 408
@@ -78,6 +73,22 @@ type Response struct {
*http.Response `json:"-"`
}
// IsHTTPStatus returns true if the returned HTTP status code matches the provided status code.
// If there was no response (i.e. the underlying http.Response is nil) the return value is false.
func (r Response) IsHTTPStatus(statusCode int) bool {
if r.Response == nil {
return false
}
return r.Response.StatusCode == statusCode
}
// HasHTTPStatus returns true if the returned HTTP status code matches one of the provided status codes.
// If there was no response (i.e. the underlying http.Response is nil) or not status codes are provided
// the return value is false.
func (r Response) HasHTTPStatus(statusCodes ...int) bool {
return ResponseHasStatusCode(r.Response, statusCodes...)
}
// LoggingInspector implements request and response inspectors that log the full request and
// response to a supplied log.
type LoggingInspector struct {
@@ -153,6 +164,7 @@ type Client struct {
PollingDelay time.Duration
// PollingDuration sets the maximum polling time after which an error is returned.
// Setting this to zero will use the provided context to control the duration.
PollingDuration time.Duration
// RetryAttempts sets the default number of retry attempts for client.
@@ -174,14 +186,32 @@ type Client struct {
// NewClientWithUserAgent returns an instance of a Client with the UserAgent set to the passed
// string.
func NewClientWithUserAgent(ua string) Client {
return newClient(ua, tls.RenegotiateNever)
}
// ClientOptions contains various Client configuration options.
type ClientOptions struct {
// UserAgent is an optional user-agent string to append to the default user agent.
UserAgent string
// Renegotiation is an optional setting to control client-side TLS renegotiation.
Renegotiation tls.RenegotiationSupport
}
// NewClientWithOptions returns an instance of a Client with the specified values.
func NewClientWithOptions(options ClientOptions) Client {
return newClient(options.UserAgent, options.Renegotiation)
}
func newClient(ua string, renegotiation tls.RenegotiationSupport) Client {
c := Client{
PollingDelay: DefaultPollingDelay,
PollingDuration: DefaultPollingDuration,
RetryAttempts: DefaultRetryAttempts,
RetryDuration: DefaultRetryDuration,
UserAgent: defaultUserAgent,
UserAgent: UserAgent(),
}
c.Sender = c.sender()
c.Sender = c.sender(renegotiation)
c.AddToUserAgent(ua)
return c
}
@@ -216,18 +246,48 @@ func (c Client) Do(r *http.Request) (*http.Response, error) {
}
return resp, NewErrorWithError(err, "autorest/Client", "Do", nil, "Preparing request failed")
}
resp, err := SendWithSender(c.sender(), r)
logger.Instance.WriteRequest(r, logger.Filter{
Header: func(k string, v []string) (bool, []string) {
// remove the auth token from the log
if strings.EqualFold(k, "Authorization") || strings.EqualFold(k, "Ocp-Apim-Subscription-Key") {
v = []string{"**REDACTED**"}
}
return true, v
},
})
resp, err := SendWithSender(c.sender(tls.RenegotiateNever), r)
logger.Instance.WriteResponse(resp, logger.Filter{})
Respond(resp, c.ByInspecting())
return resp, err
}
// sender returns the Sender to which to send requests.
func (c Client) sender() Sender {
func (c Client) sender(renengotiation tls.RenegotiationSupport) Sender {
if c.Sender == nil {
// Use behaviour compatible with DefaultTransport, but require TLS minimum version.
var defaultTransport = http.DefaultTransport.(*http.Transport)
transport := tracing.Transport
// for non-default values of TLS renegotiation create a new tracing transport.
// updating tracing.Transport affects all clients which is not what we want.
if renengotiation != tls.RenegotiateNever {
transport = tracing.NewTransport()
}
transport.Base = &http.Transport{
Proxy: defaultTransport.Proxy,
DialContext: defaultTransport.DialContext,
MaxIdleConns: defaultTransport.MaxIdleConns,
IdleConnTimeout: defaultTransport.IdleConnTimeout,
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
TLSClientConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
Renegotiation: renengotiation,
},
}
j, _ := cookiejar.New(nil)
return &http.Client{Jar: j}
return &http.Client{Jar: j, Transport: transport}
}
return c.Sender
}

View File

@@ -16,7 +16,9 @@ package autorest
import (
"bytes"
"context"
"encoding/json"
"encoding/xml"
"fmt"
"io"
"io/ioutil"
@@ -31,11 +33,33 @@ const (
mimeTypeOctetStream = "application/octet-stream"
mimeTypeFormPost = "application/x-www-form-urlencoded"
headerAuthorization = "Authorization"
headerContentType = "Content-Type"
headerUserAgent = "User-Agent"
headerAuthorization = "Authorization"
headerAuxAuthorization = "x-ms-authorization-auxiliary"
headerContentType = "Content-Type"
headerUserAgent = "User-Agent"
)
// used as a key type in context.WithValue()
type ctxPrepareDecorators struct{}
// WithPrepareDecorators adds the specified PrepareDecorators to the provided context.
// If no PrepareDecorators are provided the context is unchanged.
func WithPrepareDecorators(ctx context.Context, prepareDecorator []PrepareDecorator) context.Context {
if len(prepareDecorator) == 0 {
return ctx
}
return context.WithValue(ctx, ctxPrepareDecorators{}, prepareDecorator)
}
// GetPrepareDecorators returns the PrepareDecorators in the provided context or the provided default PrepareDecorators.
func GetPrepareDecorators(ctx context.Context, defaultPrepareDecorators ...PrepareDecorator) []PrepareDecorator {
inCtx := ctx.Value(ctxPrepareDecorators{})
if pd, ok := inCtx.([]PrepareDecorator); ok {
return pd
}
return defaultPrepareDecorators
}
// Preparer is the interface that wraps the Prepare method.
//
// Prepare accepts and possibly modifies an http.Request (e.g., adding Headers). Implementations
@@ -190,6 +214,9 @@ func AsGet() PrepareDecorator { return WithMethod("GET") }
// AsHead returns a PrepareDecorator that sets the HTTP method to HEAD.
func AsHead() PrepareDecorator { return WithMethod("HEAD") }
// AsMerge returns a PrepareDecorator that sets the HTTP method to MERGE.
func AsMerge() PrepareDecorator { return WithMethod("MERGE") }
// AsOptions returns a PrepareDecorator that sets the HTTP method to OPTIONS.
func AsOptions() PrepareDecorator { return WithMethod("OPTIONS") }
@@ -225,6 +252,25 @@ func WithBaseURL(baseURL string) PrepareDecorator {
}
}
// WithBytes returns a PrepareDecorator that takes a list of bytes
// which passes the bytes directly to the body
func WithBytes(input *[]byte) PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err == nil {
if input == nil {
return r, fmt.Errorf("Input Bytes was nil")
}
r.ContentLength = int64(len(*input))
r.Body = ioutil.NopCloser(bytes.NewReader(*input))
}
return r, err
})
}
}
// WithCustomBaseURL returns a PrepareDecorator that replaces brace-enclosed keys within the
// request base URL (i.e., http.Request.URL) with the corresponding values from the passed map.
func WithCustomBaseURL(baseURL string, urlParameters map[string]interface{}) PrepareDecorator {
@@ -377,6 +423,28 @@ func WithJSON(v interface{}) PrepareDecorator {
}
}
// WithXML returns a PrepareDecorator that encodes the data passed as XML into the body of the
// request and sets the Content-Length header.
func WithXML(v interface{}) PrepareDecorator {
return func(p Preparer) Preparer {
return PreparerFunc(func(r *http.Request) (*http.Request, error) {
r, err := p.Prepare(r)
if err == nil {
b, err := xml.Marshal(v)
if err == nil {
// we have to tack on an XML header
withHeader := xml.Header + string(b)
bytesWithHeader := []byte(withHeader)
r.ContentLength = int64(len(bytesWithHeader))
r.Body = ioutil.NopCloser(bytes.NewReader(bytesWithHeader))
}
}
return r, err
})
}
}
// WithPath returns a PrepareDecorator that adds the supplied path to the request URL. If the path
// is absolute (that is, it begins with a "/"), it replaces the existing path.
func WithPath(path string) PrepareDecorator {

View File

@@ -153,6 +153,25 @@ func ByClosingIfError() RespondDecorator {
}
}
// ByUnmarshallingBytes returns a RespondDecorator that copies the Bytes returned in the
// response Body into the value pointed to by v.
func ByUnmarshallingBytes(v *[]byte) RespondDecorator {
return func(r Responder) Responder {
return ResponderFunc(func(resp *http.Response) error {
err := r.Respond(resp)
if err == nil {
bytes, errInner := ioutil.ReadAll(resp.Body)
if errInner != nil {
err = fmt.Errorf("Error occurred reading http.Response#Body - Error = '%v'", errInner)
} else {
*v = bytes
}
}
return err
})
}
}
// ByUnmarshallingJSON returns a RespondDecorator that decodes a JSON document returned in the
// response Body into the value pointed to by v.
func ByUnmarshallingJSON(v interface{}) RespondDecorator {

View File

@@ -15,14 +15,38 @@ package autorest
// limitations under the License.
import (
"context"
"fmt"
"log"
"math"
"net/http"
"strconv"
"time"
"github.com/Azure/go-autorest/tracing"
)
// used as a key type in context.WithValue()
type ctxSendDecorators struct{}
// WithSendDecorators adds the specified SendDecorators to the provided context.
// If no SendDecorators are provided the context is unchanged.
func WithSendDecorators(ctx context.Context, sendDecorator []SendDecorator) context.Context {
if len(sendDecorator) == 0 {
return ctx
}
return context.WithValue(ctx, ctxSendDecorators{}, sendDecorator)
}
// GetSendDecorators returns the SendDecorators in the provided context or the provided default SendDecorators.
func GetSendDecorators(ctx context.Context, defaultSendDecorators ...SendDecorator) []SendDecorator {
inCtx := ctx.Value(ctxSendDecorators{})
if sd, ok := inCtx.([]SendDecorator); ok {
return sd
}
return defaultSendDecorators
}
// Sender is the interface that wraps the Do method to send HTTP requests.
//
// The standard http.Client conforms to this interface.
@@ -38,7 +62,7 @@ func (sf SenderFunc) Do(r *http.Request) (*http.Response, error) {
return sf(r)
}
// SendDecorator takes and possibily decorates, by wrapping, a Sender. Decorators may affect the
// SendDecorator takes and possibly decorates, by wrapping, a Sender. Decorators may affect the
// http.Request and pass it along or, first, pass the http.Request along then react to the
// http.Response result.
type SendDecorator func(Sender) Sender
@@ -68,7 +92,7 @@ func DecorateSender(s Sender, decorators ...SendDecorator) Sender {
//
// Send will not poll or retry requests.
func Send(r *http.Request, decorators ...SendDecorator) (*http.Response, error) {
return SendWithSender(&http.Client{}, r, decorators...)
return SendWithSender(&http.Client{Transport: tracing.Transport}, r, decorators...)
}
// SendWithSender sends the passed http.Request, through the provided Sender, returning the
@@ -209,50 +233,77 @@ func DoRetryForAttempts(attempts int, backoff time.Duration) SendDecorator {
// DoRetryForStatusCodes returns a SendDecorator that retries for specified statusCodes for up to the specified
// number of attempts, exponentially backing off between requests using the supplied backoff
// time.Duration (which may be zero). Retrying may be canceled by closing the optional channel on
// the http.Request.
// time.Duration (which may be zero). Retrying may be canceled by cancelling the context on the http.Request.
// NOTE: Code http.StatusTooManyRequests (429) will *not* be counted against the number of attempts.
func DoRetryForStatusCodes(attempts int, backoff time.Duration, codes ...int) SendDecorator {
return func(s Sender) Sender {
return SenderFunc(func(r *http.Request) (resp *http.Response, err error) {
rr := NewRetriableRequest(r)
// Increment to add the first call (attempts denotes number of retries)
attempts++
for attempt := 0; attempt < attempts; {
err = rr.Prepare()
if err != nil {
return resp, err
}
resp, err = s.Do(rr.Request())
// we want to retry if err is not nil (e.g. transient network failure). note that for failed authentication
// resp and err will both have a value, so in this case we don't want to retry as it will never succeed.
if err == nil && !ResponseHasStatusCode(resp, codes...) || IsTokenRefreshError(err) {
return resp, err
}
delayed := DelayWithRetryAfter(resp, r.Context().Done())
if !delayed && !DelayForBackoff(backoff, attempt, r.Context().Done()) {
return nil, r.Context().Err()
}
// don't count a 429 against the number of attempts
// so that we continue to retry until it succeeds
if resp == nil || resp.StatusCode != http.StatusTooManyRequests {
attempt++
}
}
return resp, err
return SenderFunc(func(r *http.Request) (*http.Response, error) {
return doRetryForStatusCodesImpl(s, r, false, attempts, backoff, 0, codes...)
})
}
}
// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header in
// responses with status code 429
// DoRetryForStatusCodesWithCap returns a SendDecorator that retries for specified statusCodes for up to the
// specified number of attempts, exponentially backing off between requests using the supplied backoff
// time.Duration (which may be zero). To cap the maximum possible delay between iterations specify a value greater
// than zero for cap. Retrying may be canceled by cancelling the context on the http.Request.
func DoRetryForStatusCodesWithCap(attempts int, backoff, cap time.Duration, codes ...int) SendDecorator {
return func(s Sender) Sender {
return SenderFunc(func(r *http.Request) (*http.Response, error) {
return doRetryForStatusCodesImpl(s, r, true, attempts, backoff, cap, codes...)
})
}
}
func doRetryForStatusCodesImpl(s Sender, r *http.Request, count429 bool, attempts int, backoff, cap time.Duration, codes ...int) (resp *http.Response, err error) {
rr := NewRetriableRequest(r)
// Increment to add the first call (attempts denotes number of retries)
for attempt := 0; attempt < attempts+1; {
err = rr.Prepare()
if err != nil {
return
}
resp, err = s.Do(rr.Request())
// if the error isn't temporary don't bother retrying
if err != nil && !IsTemporaryNetworkError(err) {
return
}
// we want to retry if err is not nil (e.g. transient network failure). note that for failed authentication
// resp and err will both have a value, so in this case we don't want to retry as it will never succeed.
if err == nil && !ResponseHasStatusCode(resp, codes...) || IsTokenRefreshError(err) {
return resp, err
}
delayed := DelayWithRetryAfter(resp, r.Context().Done())
if !delayed && !DelayForBackoffWithCap(backoff, cap, attempt, r.Context().Done()) {
return resp, r.Context().Err()
}
// when count429 == false don't count a 429 against the number
// of attempts so that we continue to retry until it succeeds
if count429 || (resp == nil || resp.StatusCode != http.StatusTooManyRequests) {
attempt++
}
}
return resp, err
}
// DelayWithRetryAfter invokes time.After for the duration specified in the "Retry-After" header.
// The value of Retry-After can be either the number of seconds or a date in RFC1123 format.
// The function returns true after successfully waiting for the specified duration. If there is
// no Retry-After header or the wait is cancelled the return value is false.
func DelayWithRetryAfter(resp *http.Response, cancel <-chan struct{}) bool {
if resp == nil {
return false
}
retryAfter, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
if resp.StatusCode == http.StatusTooManyRequests && retryAfter > 0 {
var dur time.Duration
ra := resp.Header.Get("Retry-After")
if retryAfter, _ := strconv.Atoi(ra); retryAfter > 0 {
dur = time.Duration(retryAfter) * time.Second
} else if t, err := time.Parse(time.RFC1123, ra); err == nil {
dur = t.Sub(time.Now())
}
if dur > 0 {
select {
case <-time.After(time.Duration(retryAfter) * time.Second):
case <-time.After(dur):
return true
case <-cancel:
return false
@@ -312,8 +363,22 @@ func WithLogging(logger *log.Logger) SendDecorator {
// Note: Passing attempt 1 will result in doubling "backoff" duration. Treat this as a zero-based attempt
// count.
func DelayForBackoff(backoff time.Duration, attempt int, cancel <-chan struct{}) bool {
return DelayForBackoffWithCap(backoff, 0, attempt, cancel)
}
// DelayForBackoffWithCap invokes time.After for the supplied backoff duration raised to the power of
// passed attempt (i.e., an exponential backoff delay). Backoff duration is in seconds and can set
// to zero for no delay. To cap the maximum possible delay specify a value greater than zero for cap.
// The delay may be canceled by closing the passed channel. If terminated early, returns false.
// Note: Passing attempt 1 will result in doubling "backoff" duration. Treat this as a zero-based attempt
// count.
func DelayForBackoffWithCap(backoff, cap time.Duration, attempt int, cancel <-chan struct{}) bool {
d := time.Duration(backoff.Seconds()*math.Pow(2, float64(attempt))) * time.Second
if cap > 0 && d > cap {
d = cap
}
select {
case <-time.After(time.Duration(backoff.Seconds()*math.Pow(2, float64(attempt))) * time.Second):
case <-time.After(d):
return true
case <-cancel:
return false

View File

@@ -145,3 +145,8 @@ func Float64(i *float64) float64 {
func Float64Ptr(i float64) *float64 {
return &i
}
// ByteSlicePtr returns a pointer to the passed byte slice.
func ByteSlicePtr(b []byte) *[]byte {
return &b
}

Some files were not shown because too many files have changed in this diff Show More