From 39c1cc1b3ca096423202054e07b81796051e6d06 Mon Sep 17 00:00:00 2001 From: Lawrence Gripper Date: Mon, 27 Nov 2017 13:26:04 +0000 Subject: [PATCH] Add Service Fabric Provider --- cmd/traefik/configuration.go | 11 +- configuration/configuration.go | 2 + docs/configuration/backends/servicefabric.md | 107 +++++ glide.lock | 12 +- glide.yaml | 2 + mkdocs.yml | 1 + server/server.go | 3 + .../github.com/Masterminds/sprig/functions.go | 11 + .../github.com/Masterminds/sprig/numeric.go | 30 ++ vendor/github.com/Masterminds/sprig/regex.go | 35 ++ .../traefik-extra-service-fabric/LICENSE | 201 ++++++++++ .../servicefabric.go | 358 +++++++++++++++++ .../servicefabric_tmpl.go | 141 +++++++ .../traefik-extra-service-fabric/types.go | 43 ++ vendor/github.com/imdario/mergo/map.go | 24 +- vendor/github.com/imdario/mergo/merge.go | 52 ++- vendor/github.com/imdario/mergo/mergo.go | 2 +- .../jjcollinge/servicefabric/LICENSE | 21 + .../jjcollinge/servicefabric/query.go | 20 + .../jjcollinge/servicefabric/servicefabric.go | 367 ++++++++++++++++++ .../jjcollinge/servicefabric/types.go | 199 ++++++++++ 21 files changed, 1624 insertions(+), 18 deletions(-) create mode 100644 docs/configuration/backends/servicefabric.md create mode 100644 vendor/github.com/Masterminds/sprig/regex.go create mode 100644 vendor/github.com/containous/traefik-extra-service-fabric/LICENSE create mode 100644 vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go create mode 100644 vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go create mode 100644 vendor/github.com/containous/traefik-extra-service-fabric/types.go create mode 100644 vendor/github.com/jjcollinge/servicefabric/LICENSE create mode 100644 vendor/github.com/jjcollinge/servicefabric/query.go create mode 100644 vendor/github.com/jjcollinge/servicefabric/servicefabric.go create mode 100644 vendor/github.com/jjcollinge/servicefabric/types.go diff --git a/cmd/traefik/configuration.go b/cmd/traefik/configuration.go index c2b4d0cfb..1b041d582 100644 --- a/cmd/traefik/configuration.go +++ b/cmd/traefik/configuration.go @@ -4,6 +4,7 @@ import ( "time" "github.com/containous/flaeg" + "github.com/containous/traefik-extra-service-fabric" "github.com/containous/traefik/api" "github.com/containous/traefik/configuration" "github.com/containous/traefik/middlewares/accesslog" @@ -23,6 +24,7 @@ import ( "github.com/containous/traefik/provider/rest" "github.com/containous/traefik/provider/zk" "github.com/containous/traefik/types" + sf "github.com/jjcollinge/servicefabric" ) // TraefikConfiguration holds GlobalConfiguration and other stuff @@ -163,6 +165,11 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { var defaultEureka eureka.Provider defaultEureka.Delay = "30s" + // default ServiceFabric + var defaultServiceFabric servicefabric.Provider + defaultServiceFabric.APIVersion = sf.DefaultAPIVersion + defaultServiceFabric.RefreshSeconds = 10 + // default Ping var defaultPing = ping.Handler{ EntryPoint: "traefik", @@ -196,7 +203,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { } // default LifeCycle - defaultLifeycle := configuration.LifeCycle{ + defaultLifeCycle := configuration.LifeCycle{ GraceTimeOut: flaeg.Duration(configuration.DefaultGraceTimeout), } @@ -252,7 +259,7 @@ func NewTraefikDefaultPointersConfiguration() *TraefikConfiguration { ForwardingTimeouts: &forwardingTimeouts, TraefikLog: &defaultTraefikLog, AccessLog: &defaultAccessLog, - LifeCycle: &defaultLifeycle, + LifeCycle: &defaultLifeCycle, Ping: &defaultPing, API: &defaultAPI, Metrics: &defaultMetrics, diff --git a/configuration/configuration.go b/configuration/configuration.go index 0eba373f8..199d5ee0d 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -6,6 +6,7 @@ import ( "time" "github.com/containous/flaeg" + "github.com/containous/traefik-extra-service-fabric" "github.com/containous/traefik/acme" "github.com/containous/traefik/api" "github.com/containous/traefik/log" @@ -88,6 +89,7 @@ type GlobalConfiguration struct { ECS *ecs.Provider `description:"Enable ECS backend with default settings" export:"true"` Rancher *rancher.Provider `description:"Enable Rancher backend with default settings" export:"true"` DynamoDB *dynamodb.Provider `description:"Enable DynamoDB backend with default settings" export:"true"` + ServiceFabric *servicefabric.Provider `description:"Enable Service Fabric backend with default settings" export:"true"` Rest *rest.Provider `description:"Enable Rest backend with default settings" export:"true"` API *api.Handler `description:"Enable api/dashboard" export:"true"` Metrics *types.Metrics `description:"Enable a metrics exporter" export:"true"` diff --git a/docs/configuration/backends/servicefabric.md b/docs/configuration/backends/servicefabric.md new file mode 100644 index 000000000..07ac49b72 --- /dev/null +++ b/docs/configuration/backends/servicefabric.md @@ -0,0 +1,107 @@ +# Service Fabric Backend + +Træfik can be configured to use Service Fabric as a backend configuration. + +See [this repository for an example deployment package and further documentation.](https://aka.ms/traefikonsf) + +## Service Fabric + +```toml +################################################################ +# Service Fabric provider +################################################################ + +# Enable Service Fabric configuration backend +[serviceFabric] + +# Service Fabric Management Endpoint +# +# Required +# +clusterManagementUrl = "https://localhost:19080" + +# Service Fabric Management Endpoint API Version +# +# Required +# Default: "3.0" +# +apiVersion = "3.0" + +# Enable TLS connection. +# +# Optional +# +# [serviceFabric.tls] +# ca = "/etc/ssl/ca.crt" +# cert = "/etc/ssl/servicefabric.crt" +# key = "/etc/ssl/servicefabric.key" +# insecureskipverify = true +``` + +## Labels + +The provider uses labels to configure how services are exposed through Træfik. +These can be set using Extensions and the Property Manager API + +#### Extensions + +Set labels with extensions through the services `ServiceManifest.xml` file. +Here is an example of an extension setting Træfik labels: + +```xml + + + + + + + + + + + +``` + +#### Property Manager + +Set Labels with the property manager API to overwrite and add labels, while your service is running. +Here is an example of adding a frontend rule using the property manager API. + +```shell +curl -X PUT \ + 'http://localhost:19080/Names/GettingStartedApplication2/WebService/$/GetProperty?api-version=6.0&IncludeValues=true' \ + -d '{ + "PropertyName": "traefik.frontend.rule.default", + "Value": { + "Kind": "String", + "Data": "PathPrefixStrip: /a/path/to/strip" + }, + "CustomTypeId": "LabelType" +}' +``` + +!!! note + This functionality will be released in a future version of the [sfctl](https://docs.microsoft.com/en-us/azure/service-fabric/service-fabric-application-lifecycle-sfctl) tool. + +## Available Labels + +Labels, set through extensions or the property manager, can be used on services to override default behaviour. + +| Label | Description | +|-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `traefik.backend.maxconn.amount=10` | Set a maximum number of connections to the backend.
Must be used in conjunction with the below label to take effect. | +| `traefik.backend.maxconn.extractorfunc=client.ip` | Set the function to be used against the request to determine what to limit maximum connections to the backend by.
Must be used in conjunction with the above label to take effect. | +| `traefik.backend.loadbalancer.method=drr` | Override the default `wrr` load balancer algorithm | +| `traefik.backend.loadbalancer.stickiness=true` | Enable backend sticky sessions | +| `traefik.backend.loadbalancer.stickiness.cookieName=NAME` | Manually set the cookie name for sticky sessions | +| `traefik.backend.circuitbreaker.expression=EXPR` | Create a [circuit breaker](/basics/#backends) to be used against the backend | +| `traefik.backend.weight=10` | Assign this weight to the container | +| `traefik.expose=true` | Expose this service using træfik | +| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Defaults to SF address. | +| `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | +| `traefik.frontend.priority=10` | Override default frontend priority | +| `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints` | +| `traefik.frontend.auth.basic=EXPR` | Set basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | +| `traefik.frontend.whitelistSourceRange:RANGE` | List of IP-Ranges which are allowed to access. An unset or empty list allows all Source-IPs to access.
If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | +| `traefik.backend.group.name` | Group all services with the same name into a single backend in Træfik | +| `traefik.backend.group.weight` | Set the weighting of the current services nodes in the backend group | diff --git a/glide.lock b/glide.lock index 0954ce58e..510910112 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 4ac4017b19d4a7355894a09cd6d1f2b92729f252578b6a044a3ff2dea22133c4 -updated: 2017-11-21T21:24:24.601164724+01:00 +hash: e17ccb5f089116a333c1d2ccba9f5d710bd28779b7d4461e37a1c301c2a00fba +updated: 2017-11-22T15:59:09.002650953+01:00 imports: - name: cloud.google.com/go version: 2e6a95edb1071d750f6d7db777bf66cd2997af6c @@ -93,6 +93,8 @@ imports: version: 06ccd3e75091eb659b1d720cda0e16bc7057954c - name: github.com/containous/staert version: af517d5b70db9c4b0505e0144fcc62b054057d2a +- name: github.com/containous/traefik-extra-service-fabric + version: 43b41061088161b93413eb35bd1496f11347c43b - name: github.com/coreos/bbolt version: 3c6cbfb299c11444eb2f8c9d48f0d2ce09157423 - name: github.com/coreos/etcd @@ -334,7 +336,7 @@ imports: - name: github.com/huandu/xstrings version: 3959339b333561bf62a38b424fd41517c2c90f40 - name: github.com/imdario/mergo - version: 3e95a51e0639b4cf372f2ccf74c86749d747fbdc + version: 7fe0c75c13abdee74b09fcacef5ea1c6bba6a874 - name: github.com/influxdata/influxdb version: 2d474a3089bcfce6b472779be9470a1f0ef3d5e4 subpackages: @@ -345,6 +347,8 @@ imports: version: 2fd0705ce648e602e6c9c57329a174270a4f6688 subpackages: - lib +- name: github.com/jjcollinge/servicefabric + version: 93a44e59fc887cda489913c6fc5bda834989f3bd - name: github.com/jmespath/go-jmespath version: bd40a432e4c76585ef6b72d3fd96fb9b6dc7b68d - name: github.com/jonboulle/clockwork @@ -368,7 +372,7 @@ imports: - name: github.com/Masterminds/semver version: 59c29afe1a994eacb71c833025ca7acf874bb1da - name: github.com/Masterminds/sprig - version: 9526be0327b26ad31aa70296a7b10704883976d5 + version: e039e20e500c2c025d9145be375e27cf42a94174 - name: github.com/mattn/go-colorable version: 5411d3eea5978e6cdc258b30de592b60df6aba96 repo: https://github.com/mattn/go-colorable diff --git a/glide.yaml b/glide.yaml index 43683c085..3e083db9b 100644 --- a/glide.yaml +++ b/glide.yaml @@ -11,6 +11,8 @@ import: version: 10f801ebc38b33738c9d17d50860f484a0988ff5 - package: github.com/cenk/backoff - package: github.com/containous/flaeg +- package: github.com/containous/traefik-extra-service-fabric + version: ^v1.0.0 - package: github.com/vulcand/oxy version: 7b6e758ab449705195df638765c4ca472248908a repo: https://github.com/containous/oxy.git diff --git a/mkdocs.yml b/mkdocs.yml index 509312a49..ed65da9ab 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -85,6 +85,7 @@ pages: - 'Backend: Mesos': 'configuration/backends/mesos.md' - 'Backend: Rancher': 'configuration/backends/rancher.md' - 'Backend: Rest': 'configuration/backends/rest.md' + - 'Backend: Service Fabric': 'configuration/backends/servicefabric.md' - 'Backend: Zookeeper': 'configuration/backends/zookeeper.md' - 'API / Dashboard': 'configuration/api.md' - 'Ping': 'configuration/ping.md' diff --git a/server/server.go b/server/server.go index 8d281695a..70e2f72cc 100644 --- a/server/server.go +++ b/server/server.go @@ -559,6 +559,9 @@ func (s *Server) configureProviders() { if s.globalConfiguration.DynamoDB != nil { s.providers = append(s.providers, s.globalConfiguration.DynamoDB) } + if s.globalConfiguration.ServiceFabric != nil { + s.providers = append(s.providers, s.globalConfiguration.ServiceFabric) + } } func (s *Server) startProviders() { diff --git a/vendor/github.com/Masterminds/sprig/functions.go b/vendor/github.com/Masterminds/sprig/functions.go index e7b30685c..c20175b25 100644 --- a/vendor/github.com/Masterminds/sprig/functions.go +++ b/vendor/github.com/Masterminds/sprig/functions.go @@ -183,6 +183,9 @@ var genericMap = map[string]interface{}{ "biggest": max, "max": max, "min": min, + "ceil": ceil, + "floor": floor, + "round": round, // string slices. Note that we reverse the order b/c that's better // for template processing. @@ -258,4 +261,12 @@ var genericMap = map[string]interface{}{ // Flow Control: "fail": func(msg string) (string, error) { return "", errors.New(msg) }, + + // Regex + "regexMatch": regexMatch, + "regexFindAll": regexFindAll, + "regexFind": regexFind, + "regexReplaceAll": regexReplaceAll, + "regexReplaceAllLiteral": regexReplaceAllLiteral, + "regexSplit": regexSplit, } diff --git a/vendor/github.com/Masterminds/sprig/numeric.go b/vendor/github.com/Masterminds/sprig/numeric.go index 191e3b97a..209c62e53 100644 --- a/vendor/github.com/Masterminds/sprig/numeric.go +++ b/vendor/github.com/Masterminds/sprig/numeric.go @@ -127,3 +127,33 @@ func untilStep(start, stop, step int) []int { } return v } + +func floor(a interface{}) float64 { + aa := toFloat64(a) + return math.Floor(aa) +} + +func ceil(a interface{}) float64 { + aa := toFloat64(a) + return math.Ceil(aa) +} + +func round(a interface{}, p int, r_opt ...float64) float64 { + roundOn := .5 + if len(r_opt) > 0 { + roundOn = r_opt[0] + } + val := toFloat64(a) + places := toFloat64(p) + + var round float64 + pow := math.Pow(10, places) + digit := pow * val + _, div := math.Modf(digit) + if div >= roundOn { + round = math.Ceil(digit) + } else { + round = math.Floor(digit) + } + return round / pow +} \ No newline at end of file diff --git a/vendor/github.com/Masterminds/sprig/regex.go b/vendor/github.com/Masterminds/sprig/regex.go new file mode 100644 index 000000000..9fe033a6b --- /dev/null +++ b/vendor/github.com/Masterminds/sprig/regex.go @@ -0,0 +1,35 @@ +package sprig + +import ( + "regexp" +) + +func regexMatch(regex string, s string) bool { + match, _ := regexp.MatchString(regex, s) + return match +} + +func regexFindAll(regex string, s string, n int) []string { + r := regexp.MustCompile(regex) + return r.FindAllString(s, n) +} + +func regexFind(regex string, s string) string { + r := regexp.MustCompile(regex) + return r.FindString(s) +} + +func regexReplaceAll(regex string, s string, repl string) string { + r := regexp.MustCompile(regex) + return r.ReplaceAllString(s, repl) +} + +func regexReplaceAllLiteral(regex string, s string, repl string) string { + r := regexp.MustCompile(regex) + return r.ReplaceAllLiteralString(s, repl) +} + +func regexSplit(regex string, s string, n int) []string { + r := regexp.MustCompile(regex) + return r.Split(s, n) +} \ No newline at end of file diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/LICENSE b/vendor/github.com/containous/traefik-extra-service-fabric/LICENSE new file mode 100644 index 000000000..63ea16409 --- /dev/null +++ b/vendor/github.com/containous/traefik-extra-service-fabric/LICENSE @@ -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 2017 Containous + + 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. diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go new file mode 100644 index 000000000..487fb6ecc --- /dev/null +++ b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric.go @@ -0,0 +1,358 @@ +package servicefabric + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + "text/template" + "time" + + "github.com/cenk/backoff" + "github.com/containous/traefik/job" + "github.com/containous/traefik/log" + "github.com/containous/traefik/provider" + "github.com/containous/traefik/safe" + "github.com/containous/traefik/types" + sf "github.com/jjcollinge/servicefabric" +) + +var _ provider.Provider = (*Provider)(nil) + +const traefikLabelPrefix = "traefik" + +// Provider holds for configuration for the provider +type Provider struct { + provider.BaseProvider `mapstructure:",squash"` + ClusterManagementURL string `description:"Service Fabric API endpoint"` + APIVersion string `description:"Service Fabric API version" export:"true"` + RefreshSeconds int `description:"Polling interval (in seconds)" export:"true"` + TLS *types.ClientTLS `description:"Enable TLS support" export:"true"` +} + +// Provide allows the ServiceFabric provider to provide configurations to traefik +// using the given configuration channel. +func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error { + if p.APIVersion == "" { + p.APIVersion = sf.DefaultAPIVersion + } + + tlsConfig, err := p.TLS.CreateTLSConfig() + if err != nil { + return err + } + + sfClient, err := sf.NewClient(http.DefaultClient, p.ClusterManagementURL, p.APIVersion, tlsConfig) + if err != nil { + return err + } + + if p.RefreshSeconds <= 0 { + p.RefreshSeconds = 10 + } + + return p.updateConfig(configurationChan, pool, sfClient, time.Duration(p.RefreshSeconds)*time.Second) +} + +func (p *Provider) updateConfig(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, sfClient sfClient, pollInterval time.Duration) error { + pool.Go(func(stop chan bool) { + operation := func() error { + ticker := time.NewTicker(pollInterval) + for range ticker.C { + select { + case shouldStop := <-stop: + if shouldStop { + ticker.Stop() + return nil + } + default: + log.Info("Checking service fabric config") + } + + services, err := getClusterServices(sfClient) + if err != nil { + return err + } + + templateObjects := struct { + Services []ServiceItemExtended + }{ + services, + } + + var sfFuncMap = template.FuncMap{ + "isPrimary": isPrimary, + "getDefaultEndpoint": p.getDefaultEndpoint, + "getNamedEndpoint": p.getNamedEndpoint, + "getApplicationParameter": p.getApplicationParameter, + "doesAppParamContain": p.doesAppParamContain, + "hasServiceLabel": hasServiceLabel, + "getServiceLabelValue": getServiceLabelValue, + "getServiceLabelValueWithDefault": getServiceLabelValueWithDefault, + "getServiceLabelsWithPrefix": getServiceLabelsWithPrefix, + "getServicesWithLabelValueMap": getServicesWithLabelValueMap, + "getServicesWithLabelValue": getServicesWithLabelValue, + } + + configuration, err := p.GetConfiguration(tmpl, sfFuncMap, templateObjects) + + if err != nil { + return err + } + + configurationChan <- types.ConfigMessage{ + ProviderName: "servicefabric", + Configuration: configuration, + } + } + return nil + } + + notify := func(err error, time time.Duration) { + log.Errorf("Provider connection error: %v; retrying in %s", err, time) + } + err := backoff.RetryNotify(safe.OperationWithRecover(operation), job.NewBackOff(backoff.NewExponentialBackOff()), notify) + if err != nil { + log.Errorf("Cannot connect to Provider: %v", err) + } + }) + return nil +} + +func (p Provider) doesAppParamContain(app sf.ApplicationItem, key, shouldContain string) bool { + value := p.getApplicationParameter(app, key) + return strings.Contains(value, shouldContain) +} + +func (p Provider) getApplicationParameter(app sf.ApplicationItem, key string) string { + for _, param := range app.Parameters { + if param.Key == key { + return param.Value + } + } + log.Errorf("Parameter %s doesn't exist in app %s", key, app.Name) + return "" +} + +func (p Provider) getDefaultEndpoint(instance replicaInstance) string { + id, data := instance.GetReplicaData() + endpoint, err := getDefaultEndpoint(data.Address) + if err != nil { + log.Warnf("No default endpoint for replica %s in service %s endpointData: %s", id, data.Address) + return "" + } + return endpoint +} + +func (p Provider) getNamedEndpoint(instance replicaInstance, endpointName string) string { + id, data := instance.GetReplicaData() + endpoint, err := getNamedEndpoint(data.Address, endpointName) + if err != nil { + log.Warnf("No names endpoint of %s for replica %s in endpointData: %s", endpointName, id, data.Address) + return "" + } + return endpoint +} + +func getClusterServices(sfClient sfClient) ([]ServiceItemExtended, error) { + apps, err := sfClient.GetApplications() + if err != nil { + return nil, err + } + + var results []ServiceItemExtended + for _, app := range apps.Items { + services, err := sfClient.GetServices(app.ID) + if err != nil { + return nil, err + } + + for _, service := range services.Items { + item := ServiceItemExtended{ + ServiceItem: service, + Application: app, + } + + if labels, err := sfClient.GetServiceLabels(&service, &app, traefikLabelPrefix); err != nil { + log.Error(err) + } else { + item.Labels = labels + } + + if partitions, err := sfClient.GetPartitions(app.ID, service.ID); err != nil { + log.Error(err) + } else { + for _, partition := range partitions.Items { + partitionExt := PartitionItemExtended{PartitionItem: partition} + + if partition.ServiceKind == "Stateful" { + partitionExt.Replicas = getValidReplicas(sfClient, app, service, partition) + } else if partition.ServiceKind == "Stateless" { + partitionExt.Instances = getValidInstances(sfClient, app, service, partition) + } else { + log.Errorf("Unsupported service kind %s in service %s", partition.ServiceKind, service.Name) + continue + } + + item.Partitions = append(item.Partitions, partitionExt) + } + } + + results = append(results, item) + } + } + + return results, nil +} + +func getValidReplicas(sfClient sfClient, app sf.ApplicationItem, service sf.ServiceItem, partition sf.PartitionItem) []sf.ReplicaItem { + var validReplicas []sf.ReplicaItem + + if replicas, err := sfClient.GetReplicas(app.ID, service.ID, partition.PartitionInformation.ID); err != nil { + log.Error(err) + } else { + for _, instance := range replicas.Items { + if isHealthy(instance.ReplicaItemBase) && hasHTTPEndpoint(instance.ReplicaItemBase) { + validReplicas = append(validReplicas, instance) + } + } + } + return validReplicas +} + +func getValidInstances(sfClient sfClient, app sf.ApplicationItem, service sf.ServiceItem, partition sf.PartitionItem) []sf.InstanceItem { + var validInstances []sf.InstanceItem + + if instances, err := sfClient.GetInstances(app.ID, service.ID, partition.PartitionInformation.ID); err != nil { + log.Error(err) + } else { + for _, instance := range instances.Items { + if isHealthy(instance.ReplicaItemBase) && hasHTTPEndpoint(instance.ReplicaItemBase) { + validInstances = append(validInstances, instance) + } + } + } + return validInstances +} + +func hasServiceLabel(service ServiceItemExtended, key string) bool { + _, exists := service.Labels[key] + return exists +} + +func getServiceLabelValue(service ServiceItemExtended, key string) string { + return service.Labels[key] +} + +func getServicesWithLabelValueMap(services []ServiceItemExtended, key string) map[string][]ServiceItemExtended { + result := map[string][]ServiceItemExtended{} + for _, service := range services { + if value, exists := service.Labels[key]; exists { + if matchingServices, hasKeyAlready := result[value]; hasKeyAlready { + result[value] = append(matchingServices, service) + } else { + result[value] = []ServiceItemExtended{service} + } + } + } + return result +} + +func getServicesWithLabelValue(services []ServiceItemExtended, key, expectedValue string) []ServiceItemExtended { + var srvWithLabel []ServiceItemExtended + for _, service := range services { + value, exists := service.Labels[key] + if exists && value == expectedValue { + srvWithLabel = append(srvWithLabel, service) + } + } + return srvWithLabel +} + +func getServiceLabelValueWithDefault(service ServiceItemExtended, key, defaultValue string) string { + value, exists := service.Labels[key] + + if !exists { + return defaultValue + } + return value +} + +func getServiceLabelsWithPrefix(service ServiceItemExtended, prefix string) map[string]string { + results := make(map[string]string) + for k, v := range service.Labels { + if strings.HasPrefix(k, prefix) { + results[k] = v + } + } + return results +} + +func isPrimary(instance replicaInstance) bool { + _, data := instance.GetReplicaData() + return data.ReplicaRole == "Primary" +} + +func isHealthy(instanceData *sf.ReplicaItemBase) bool { + return instanceData != nil && (instanceData.ReplicaStatus == "Ready" || instanceData.HealthState != "Error") +} + +func hasHTTPEndpoint(instanceData *sf.ReplicaItemBase) bool { + _, err := getDefaultEndpoint(instanceData.Address) + return err == nil +} + +func decodeEndpointData(endpointData string) (map[string]string, error) { + var endpointsMap map[string]map[string]string + + if endpointData == "" { + return nil, errors.New("endpoint data is empty") + } + + err := json.Unmarshal([]byte(endpointData), &endpointsMap) + if err != nil { + return nil, err + } + + endpoints, endpointsExist := endpointsMap["Endpoints"] + if !endpointsExist { + return nil, errors.New("endpoint doesn't exist in endpoint data") + } + + return endpoints, nil +} + +func getDefaultEndpoint(endpointData string) (string, error) { + endpoints, err := decodeEndpointData(endpointData) + if err != nil { + return "", err + } + + var defaultHTTPEndpointExists bool + var defaultHTTPEndpoint string + for _, v := range endpoints { + if strings.Contains(v, "http") { + defaultHTTPEndpoint = v + defaultHTTPEndpointExists = true + break + } + } + + if !defaultHTTPEndpointExists { + return "", errors.New("no default endpoint found") + } + return defaultHTTPEndpoint, nil +} + +func getNamedEndpoint(endpointData string, endpointName string) (string, error) { + endpoints, err := decodeEndpointData(endpointData) + if err != nil { + return "", err + } + + endpoint, exists := endpoints[endpointName] + if !exists { + return "", errors.New("endpoint doesn't exist") + } + return endpoint, nil +} diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go new file mode 100644 index 000000000..64269f8c0 --- /dev/null +++ b/vendor/github.com/containous/traefik-extra-service-fabric/servicefabric_tmpl.go @@ -0,0 +1,141 @@ +package servicefabric + +const tmpl = ` +[backends] + {{$groupedServiceMap := getServicesWithLabelValueMap .Services "backend.group.name"}} + {{range $aggName, $aggServices := $groupedServiceMap }} + [backends."{{$aggName}}"] + {{range $service := $aggServices}} + {{range $partition := $service.Partitions}} + {{range $instance := $partition.Instances}} + [backends."{{$aggName}}".servers."{{$service.ID}}-{{$instance.ID}}"] + url = "{{getDefaultEndpoint $instance}}" + weight = {{getServiceLabelValueWithDefault $service "backend.group.weight" "1"}} + {{end}} + {{end}} + {{end}} + {{end}} + {{range $service := .Services}} + {{range $partition := $service.Partitions}} + {{if eq $partition.ServiceKind "Stateless"}} + [backends."{{$service.Name}}"] + [backends."{{$service.Name}}".LoadBalancer] + {{if hasServiceLabel $service "backend.loadbalancer.method"}} + method = "{{getServiceLabelValue $service "backend.loadbalancer.method" }}" + {{else}} + method = "drr" + {{end}} + + {{if hasServiceLabel $service "backend.healthcheck"}} + [backends."{{$service.Name}}".healthcheck] + path = "{{getServiceLabelValue $service "backend.healthcheck"}}" + interval = "{{getServiceLabelValueWithDefault $service "backend.healthcheck.interval" "10s"}}" + {{end}} + + {{if hasServiceLabel $service "backend.loadbalancer.stickiness"}} + [backends."{{$service.Name}}".LoadBalancer.stickiness] + {{end}} + + {{if hasServiceLabel $service "backend.circuitbreaker"}} + [backends."{{$service.Name}}".circuitbreaker] + expression = "{{getServiceLabelValue $service "backend.circuitbreaker"}}" + {{end}} + + {{if hasServiceLabel $service "backend.maxconn.amount"}} + [backends."{{$service.Name}}".maxconn] + amount = {{getServiceLabelValue $service "backend.maxconn.amount"}} + {{if hasServiceLabel $service "backend.maxconn.extractorfunc"}} + extractorfunc = "{{getServiceLabelValue $service "backend.maxconn.extractorfunc"}}" + {{end}} + {{end}} + + {{range $instance := $partition.Instances}} + [backends."{{$service.Name}}".servers."{{$instance.ID}}"] + url = "{{getDefaultEndpoint $instance}}" + weight = {{getServiceLabelValueWithDefault $service "backend.weight" "1"}} + {{end}} + {{else if eq $partition.ServiceKind "Stateful"}} + {{range $replica := $partition.Replicas}} + {{if isPrimary $replica}} + + {{$backendName := (print $service.Name $partition.PartitionInformation.ID)}} + [backends."{{$backendName}}".servers."{{$replica.ID}}"] + url = "{{getDefaultEndpoint $replica}}" + weight = 1 + + [backends."{{$backendName}}".LoadBalancer] + method = "drr" + + [backends."{{$backendName}}".circuitbreaker] + expression = "NetworkErrorRatio() > 0.5" + + {{end}} + {{end}} + {{end}} + {{end}} +{{end}} + +[frontends] +{{range $groupName, $groupServices := $groupedServiceMap}} + {{$service := index $groupServices 0}} + [frontends."{{$groupName}}"] + backend = "{{$groupName}}" + + {{if hasServiceLabel $service "frontend.priority"}} + priority = 100 + {{end}} + + {{range $key, $value := getServiceLabelsWithPrefix $service "frontend.rule"}} + [frontends."{{$groupName}}".routes."{{$key}}"] + rule = "{{$value}}" + {{end}} +{{end}} +{{range $service := .Services}} + {{if hasServiceLabel $service "expose"}} + {{if eq $service.ServiceKind "Stateless"}} + + [frontends."{{$service.Name}}"] + backend = "{{$service.Name}}" + + + {{if hasServiceLabel $service "frontend.passHostHeader"}} + passHostHeader = {{getServiceLabelValue $service "frontend.passHostHeader" }} + {{end}} + + {{if hasServiceLabel $service "frontend.whitelistSourceRange"}} + whitelistSourceRange = {{getServiceLabelValue $service "frontend.whitelistSourceRange" }} + {{end}} + + {{if hasServiceLabel $service "frontend.priority"}} + priority = {{getServiceLabelValue $service "frontend.priority"}} + {{end}} + + {{if hasServiceLabel $service "frontend.basicAuth"}} + basicAuth = {{getServiceLabelValue $service "frontend.basicAuth"}} + {{end}} + + {{if hasServiceLabel $service "frontend.entryPoints"}} + entryPoints = {{getServiceLabelValue $service "frontend.entryPoints"}} + {{end}} + + {{range $key, $value := getServiceLabelsWithPrefix $service "frontend.rule"}} + [frontends."{{$service.Name}}".routes."{{$key}}"] + rule = "{{$value}}" + {{end}} + + {{else if eq $service.ServiceKind "Stateful"}} + {{range $partition := $service.Partitions}} + {{$partitionId := $partition.PartitionInformation.ID}} + + {{if hasServiceLabel $service "frontend.rule"}} + [frontends."{{$service.Name}}/{{$partitionId}}"] + backend = "{{$service.Name}}/{{$partitionId}}" + [frontends."{{$service.Name}}/{{$partitionId}}".routes.default] + rule = {{getServiceLabelValue $service "frontend.rule.partition.$partitionId"}} + + {{end}} + {{end}} + {{end}} +{{end}} +{{end}} +` diff --git a/vendor/github.com/containous/traefik-extra-service-fabric/types.go b/vendor/github.com/containous/traefik-extra-service-fabric/types.go new file mode 100644 index 000000000..1681314d7 --- /dev/null +++ b/vendor/github.com/containous/traefik-extra-service-fabric/types.go @@ -0,0 +1,43 @@ +package servicefabric + +import ( + sf "github.com/jjcollinge/servicefabric" +) + +// ServiceItemExtended provides a flattened view +// of the service with details of the application +// it belongs too and the replicas/partitions +type ServiceItemExtended struct { + sf.ServiceItem + HasHTTPEndpoint bool + IsHealthy bool + Application sf.ApplicationItem + Partitions []PartitionItemExtended + Labels map[string]string +} + +// PartitionItemExtended provides a flattened view +// of a services partitions +type PartitionItemExtended struct { + sf.PartitionItem + Replicas []sf.ReplicaItem + Instances []sf.InstanceItem +} + +// sfClient is an interface for Service Fabric client's to implement. +// This is purposely a subset of the total Service Fabric API surface. +type sfClient interface { + GetApplications() (*sf.ApplicationItemsPage, error) + GetServices(appName string) (*sf.ServiceItemsPage, error) + GetPartitions(appName, serviceName string) (*sf.PartitionItemsPage, error) + GetReplicas(appName, serviceName, partitionName string) (*sf.ReplicaItemsPage, error) + GetInstances(appName, serviceName, partitionName string) (*sf.InstanceItemsPage, error) + GetServiceExtension(appType, applicationVersion, serviceTypeName, extensionKey string, response interface{}) error + GetServiceLabels(service *sf.ServiceItem, app *sf.ApplicationItem, prefix string) (map[string]string, error) +} + +// replicaInstance interface provides a unified interface +// over replicas and instances +type replicaInstance interface { + GetReplicaData() (string, *sf.ReplicaItemBase) +} diff --git a/vendor/github.com/imdario/mergo/map.go b/vendor/github.com/imdario/mergo/map.go index 8e8c4ba8e..99002565f 100644 --- a/vendor/github.com/imdario/mergo/map.go +++ b/vendor/github.com/imdario/mergo/map.go @@ -61,6 +61,13 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, over dstMap[fieldName] = src.Field(i).Interface() } } + case reflect.Ptr: + if dst.IsNil() { + v := reflect.New(dst.Type().Elem()) + dst.Set(v) + } + dst = dst.Elem() + fallthrough case reflect.Struct: srcMap := src.Interface().(map[string]interface{}) for key := range srcMap { @@ -85,6 +92,7 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, over srcKind = reflect.Ptr } } + if !srcElement.IsValid() { continue } @@ -92,14 +100,16 @@ func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int, over if err = deepMerge(dstElement, srcElement, visited, depth+1, overwrite); err != nil { return } - } else { - if srcKind == reflect.Map { - if err = deepMap(dstElement, srcElement, visited, depth+1, overwrite); err != nil { - return - } - } else { - return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind) + } else if dstKind == reflect.Interface && dstElement.Kind() == reflect.Interface { + if err = deepMerge(dstElement, srcElement, visited, depth+1, overwrite); err != nil { + return } + } else if srcKind == reflect.Map { + if err = deepMap(dstElement, srcElement, visited, depth+1, overwrite); err != nil { + return + } + } else { + return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind) } } } diff --git a/vendor/github.com/imdario/mergo/merge.go b/vendor/github.com/imdario/mergo/merge.go index 11e55b1e2..052b9fe78 100644 --- a/vendor/github.com/imdario/mergo/merge.go +++ b/vendor/github.com/imdario/mergo/merge.go @@ -12,6 +12,18 @@ import ( "reflect" ) +func hasExportedField(dst reflect.Value) (exported bool) { + for i, n := 0, dst.NumField(); i < n; i++ { + field := dst.Type().Field(i) + if field.Anonymous { + exported = exported || hasExportedField(dst.Field(i)) + } else { + exported = exported || len(field.PkgPath) == 0 + } + } + return +} + // Traverses recursively both values, assigning src's fields values to dst. // The map argument tracks comparisons that have already been seen, which allows // short circuiting on recursive types. @@ -34,12 +46,22 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, ov } switch dst.Kind() { case reflect.Struct: - for i, n := 0, dst.NumField(); i < n; i++ { - if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1, overwrite); err != nil { - return + if hasExportedField(dst) { + for i, n := 0, dst.NumField(); i < n; i++ { + if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1, overwrite); err != nil { + return + } + } + } else { + if dst.CanSet() && !isEmptyValue(src) && (overwrite || isEmptyValue(dst)) { + dst.Set(src) } } case reflect.Map: + if len(src.MapKeys()) == 0 && !src.IsNil() && len(dst.MapKeys()) == 0 { + dst.Set(reflect.MakeMap(dst.Type())) + return + } for _, key := range src.MapKeys() { srcElement := src.MapIndex(key) if !srcElement.IsValid() { @@ -67,6 +89,10 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, ov } } } + if dstElement.IsValid() && reflect.TypeOf(srcElement.Interface()).Kind() == reflect.Map { + continue + } + if !isEmptyValue(srcElement) && (overwrite || (!dstElement.IsValid() || isEmptyValue(dst))) { if dst.IsNil() { dst.Set(reflect.MakeMap(dst.Type())) @@ -77,9 +103,27 @@ func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int, ov case reflect.Ptr: fallthrough case reflect.Interface: + if src.Kind() != reflect.Interface { + if dst.IsNil() || overwrite { + if dst.CanSet() && (overwrite || isEmptyValue(dst)) { + dst.Set(src) + } + } else if src.Kind() == reflect.Ptr { + if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1, overwrite); err != nil { + return + } + } else if dst.Elem().Type() == src.Type() { + if err = deepMerge(dst.Elem(), src, visited, depth+1, overwrite); err != nil { + return + } + } else { + return ErrDifferentArgumentsTypes + } + break + } if src.IsNil() { break - } else if dst.IsNil() { + } else if dst.IsNil() || overwrite { if dst.CanSet() && (overwrite || isEmptyValue(dst)) { dst.Set(src) } diff --git a/vendor/github.com/imdario/mergo/mergo.go b/vendor/github.com/imdario/mergo/mergo.go index f8a0991ec..79ccdf5cb 100644 --- a/vendor/github.com/imdario/mergo/mergo.go +++ b/vendor/github.com/imdario/mergo/mergo.go @@ -45,7 +45,7 @@ func isEmptyValue(v reflect.Value) bool { return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 - case reflect.Interface, reflect.Ptr: + case reflect.Interface, reflect.Ptr, reflect.Func: return v.IsNil() } return false diff --git a/vendor/github.com/jjcollinge/servicefabric/LICENSE b/vendor/github.com/jjcollinge/servicefabric/LICENSE new file mode 100644 index 000000000..699b19cf4 --- /dev/null +++ b/vendor/github.com/jjcollinge/servicefabric/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Joni Collinge + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/jjcollinge/servicefabric/query.go b/vendor/github.com/jjcollinge/servicefabric/query.go new file mode 100644 index 000000000..1310a378b --- /dev/null +++ b/vendor/github.com/jjcollinge/servicefabric/query.go @@ -0,0 +1,20 @@ +package servicefabric + +type queryParamsFunc func(params []string) []string + +func withContinue(token string) queryParamsFunc { + if len(token) == 0 { + return noOp + } + return withParam("continue", token) +} + +func withParam(name, value string) queryParamsFunc { + return func(params []string) []string { + return append(params, name+"="+value) + } +} + +func noOp(params []string) []string { + return params +} diff --git a/vendor/github.com/jjcollinge/servicefabric/servicefabric.go b/vendor/github.com/jjcollinge/servicefabric/servicefabric.go new file mode 100644 index 000000000..a46b3ef56 --- /dev/null +++ b/vendor/github.com/jjcollinge/servicefabric/servicefabric.go @@ -0,0 +1,367 @@ +// Package servicefabric is an opinionated Service Fabric client written in Golang +package servicefabric + +import ( + "crypto/tls" + "encoding/json" + "encoding/xml" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strings" +) + +const DefaultAPIVersion = "3.0" + +// Client for Service Fabric. +// This is purposely a subset of the total Service Fabric API surface. +type Client struct { + // endpoint Service Fabric cluster management endpoint + endpoint string + // apiVersion Service Fabric API version + apiVersion string + // httpClient HTTP client + httpClient *http.Client +} + +// NewClient returns a new provider client that can query the +// Service Fabric management API externally or internally +func NewClient(httpClient *http.Client, endpoint, apiVersion string, tlsConfig *tls.Config) (*Client, error) { + if endpoint == "" { + return nil, errors.New("endpoint missing for httpClient configuration") + } + if apiVersion == "" { + apiVersion = DefaultAPIVersion + } + + if tlsConfig != nil { + tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient + tlsConfig.BuildNameToCertificate() + httpClient.Transport = &http.Transport{TLSClientConfig: tlsConfig} + } + + return &Client{ + endpoint: endpoint, + apiVersion: apiVersion, + httpClient: httpClient, + }, nil +} + +// GetApplications returns all the registered applications +// within the Service Fabric cluster. +func (c Client) GetApplications() (*ApplicationItemsPage, error) { + var aggregateAppItemsPages ApplicationItemsPage + var continueToken string + for { + res, err := c.getHTTP("Applications/", withContinue(continueToken)) + if err != nil { + return nil, err + } + + var appItemsPage ApplicationItemsPage + err = json.Unmarshal(res, &appItemsPage) + if err != nil { + return nil, fmt.Errorf("could not deserialise JSON response: %+v", err) + } + + aggregateAppItemsPages.Items = append(aggregateAppItemsPages.Items, appItemsPage.Items...) + + continueToken = getString(appItemsPage.ContinuationToken) + if continueToken == "" { + break + } + } + return &aggregateAppItemsPages, nil +} + +// GetServices returns all the services associated +// with a Service Fabric application. +func (c Client) GetServices(appName string) (*ServiceItemsPage, error) { + var aggregateServiceItemsPages ServiceItemsPage + var continueToken string + for { + res, err := c.getHTTP("Applications/"+appName+"/$/GetServices", withContinue(continueToken)) + if err != nil { + return nil, err + } + + var servicesItemsPage ServiceItemsPage + err = json.Unmarshal(res, &servicesItemsPage) + if err != nil { + return nil, fmt.Errorf("could not deserialise JSON response: %+v", err) + } + + aggregateServiceItemsPages.Items = append(aggregateServiceItemsPages.Items, servicesItemsPage.Items...) + + continueToken = getString(servicesItemsPage.ContinuationToken) + if continueToken == "" { + break + } + } + return &aggregateServiceItemsPages, nil +} + +// GetPartitions returns all the partitions associated +// with a Service Fabric service. +func (c Client) GetPartitions(appName, serviceName string) (*PartitionItemsPage, error) { + var aggregatePartitionItemsPages PartitionItemsPage + var continueToken string + for { + basePath := "Applications/" + appName + "/$/GetServices/" + serviceName + "/$/GetPartitions/" + res, err := c.getHTTP(basePath, withContinue(continueToken)) + if err != nil { + return nil, err + } + + var partitionsItemsPage PartitionItemsPage + err = json.Unmarshal(res, &partitionsItemsPage) + if err != nil { + return nil, fmt.Errorf("could not deserialise JSON response: %+v", err) + } + + aggregatePartitionItemsPages.Items = append(aggregatePartitionItemsPages.Items, partitionsItemsPage.Items...) + + continueToken = getString(partitionsItemsPage.ContinuationToken) + if continueToken == "" { + break + } + } + return &aggregatePartitionItemsPages, nil +} + +// GetInstances returns all the instances associated +// with a stateless Service Fabric partition. +func (c Client) GetInstances(appName, serviceName, partitionName string) (*InstanceItemsPage, error) { + var aggregateInstanceItemsPages InstanceItemsPage + var continueToken string + for { + basePath := "Applications/" + appName + "/$/GetServices/" + serviceName + "/$/GetPartitions/" + partitionName + "/$/GetReplicas" + res, err := c.getHTTP(basePath, withContinue(continueToken)) + if err != nil { + return nil, err + } + + var instanceItemsPage InstanceItemsPage + err = json.Unmarshal(res, &instanceItemsPage) + if err != nil { + return nil, fmt.Errorf("could not deserialise JSON response: %+v", err) + } + + aggregateInstanceItemsPages.Items = append(aggregateInstanceItemsPages.Items, instanceItemsPage.Items...) + + continueToken = getString(instanceItemsPage.ContinuationToken) + if continueToken == "" { + break + } + } + return &aggregateInstanceItemsPages, nil +} + +// GetReplicas returns all the replicas associated +// with a stateful Service Fabric partition. +func (c Client) GetReplicas(appName, serviceName, partitionName string) (*ReplicaItemsPage, error) { + var aggregateReplicaItemsPages ReplicaItemsPage + var continueToken string + for { + basePath := "Applications/" + appName + "/$/GetServices/" + serviceName + "/$/GetPartitions/" + partitionName + "/$/GetReplicas" + res, err := c.getHTTP(basePath, withContinue(continueToken)) + if err != nil { + return nil, err + } + + var replicasItemsPage ReplicaItemsPage + err = json.Unmarshal(res, &replicasItemsPage) + if err != nil { + return nil, fmt.Errorf("could not deserialise JSON response: %+v", err) + } + + aggregateReplicaItemsPages.Items = append(aggregateReplicaItemsPages.Items, replicasItemsPage.Items...) + + continueToken = getString(replicasItemsPage.ContinuationToken) + if continueToken == "" { + break + } + } + return &aggregateReplicaItemsPages, nil +} + +// GetServiceExtension returns all the extensions specified +// in a Service's manifest file. If the XML schema does not +// map to the provided interface, the default type interface will +// be returned. +func (c Client) GetServiceExtension(appType, applicationVersion, serviceTypeName, extensionKey string, response interface{}) error { + res, err := c.getHTTP("ApplicationTypes/"+appType+"/$/GetServiceTypes", withParam("ApplicationTypeVersion", applicationVersion)) + if err != nil { + return fmt.Errorf("error requesting service extensions: %v", err) + } + + var serviceTypes []ServiceType + err = json.Unmarshal(res, &serviceTypes) + if err != nil { + return fmt.Errorf("could not deserialise JSON response: %+v", err) + } + + for _, serviceTypeInfo := range serviceTypes { + if serviceTypeInfo.ServiceTypeDescription.ServiceTypeName == serviceTypeName { + for _, extension := range serviceTypeInfo.ServiceTypeDescription.Extensions { + if strings.EqualFold(extension.Key, extensionKey) { + err = xml.Unmarshal([]byte(extension.Value), &response) + if err != nil { + return fmt.Errorf("could not deserialise extension's XML value: %+v", err) + } + return nil + } + } + } + } + return nil +} + +// GetProperties uses the Property Manager API to retrieve +// string properties from a name as a dictionary +func (c Client) GetProperties(name string) (bool, map[string]string, error) { + nameExists, err := c.nameExists(name) + if err != nil { + return false, nil, err + } + + if !nameExists { + return false, nil, nil + } + + properties := make(map[string]string) + + var continueToken string + for { + res, err := c.getHTTP("Names/"+name+"/$/GetProperties", withContinue(continueToken), withParam("IncludeValues", "true")) + if err != nil { + return false, nil, err + } + + var propertiesListPage PropertiesListPage + err = json.Unmarshal(res, &propertiesListPage) + if err != nil { + return false, nil, fmt.Errorf("could not deserialise JSON response: %+v", err) + } + + for _, property := range propertiesListPage.Properties { + if property.Value.Kind != "String" { + continue + } + properties[property.Name] = property.Value.Data + } + + continueToken = propertiesListPage.ContinuationToken + if continueToken == "" { + break + } + } + + return true, properties, nil +} + +// GetServiceLabels add labels from service manifest extensions and properties manager +// expects extension xml in +func (c Client) GetServiceLabels(service *ServiceItem, app *ApplicationItem, prefix string) (map[string]string, error) { + extensionData := ServiceExtensionLabels{} + err := c.GetServiceExtension(app.TypeName, app.TypeVersion, service.TypeName, prefix, &extensionData) + if err != nil { + return nil, err + } + + prefixPeriod := prefix + "." + + labels := map[string]string{} + if extensionData.Label != nil { + for _, label := range extensionData.Label { + if strings.HasPrefix(label.Key, prefixPeriod) { + labelKey := strings.Replace(label.Key, prefixPeriod, "", -1) + labels[labelKey] = label.Value + } + } + } + + exists, properties, err := c.GetProperties(service.ID) + if err != nil { + return nil, err + } + + if exists { + for k, v := range properties { + if strings.HasPrefix(k, prefixPeriod) { + labelKey := strings.Replace(k, prefixPeriod, "", -1) + labels[labelKey] = v + } + } + } + + return labels, nil +} + +func (c Client) nameExists(propertyName string) (bool, error) { + res, err := c.getHTTPRaw("Names/" + propertyName) + // Get http will return error for any non 200 response code. + if err != nil { + return false, err + } + + return res.StatusCode == http.StatusOK, nil +} + +func (c Client) getHTTP(basePath string, paramsFuncs ...queryParamsFunc) ([]byte, error) { + if c.httpClient == nil { + return nil, errors.New("invalid http client provided") + } + + url := c.getURL(basePath, paramsFuncs...) + res, err := c.httpClient.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to connect to Service Fabric server %+v on %s", err, url) + } + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Service Fabric responded with error code %s to request %s with body %v", res.Status, url, res.Body) + } + + if res.Body == nil { + return nil, errors.New("empty response body from Service Fabric") + } + defer res.Body.Close() + + body, readErr := ioutil.ReadAll(res.Body) + if readErr != nil { + return nil, fmt.Errorf("failed to read response body from Service Fabric response %+v", readErr) + } + return body, nil +} + +func (c Client) getHTTPRaw(basePath string) (*http.Response, error) { + if c.httpClient == nil { + return nil, fmt.Errorf("invalid http client provided") + } + + url := c.getURL(basePath) + + res, err := c.httpClient.Get(url) + if err != nil { + return nil, fmt.Errorf("failed to connect to Service Fabric server %+v on %s", err, url) + } + return res, nil +} + +func (c Client) getURL(basePath string, paramsFuncs ...queryParamsFunc) string { + params := []string{"api-version=" + c.apiVersion} + + for _, paramsFunc := range paramsFuncs { + params = paramsFunc(params) + } + + return fmt.Sprintf("%s/%s?%s", c.endpoint, basePath, strings.Join(params, "&")) +} + +func getString(str *string) string { + if str == nil { + return "" + } + return *str +} diff --git a/vendor/github.com/jjcollinge/servicefabric/types.go b/vendor/github.com/jjcollinge/servicefabric/types.go new file mode 100644 index 000000000..2ec150c1f --- /dev/null +++ b/vendor/github.com/jjcollinge/servicefabric/types.go @@ -0,0 +1,199 @@ +package servicefabric + +import "encoding/xml" + +// ApplicationItemsPage encapsulates the paged response +// model for Applications in the Service Fabric API +type ApplicationItemsPage struct { + ContinuationToken *string `json:"ContinuationToken"` + Items []ApplicationItem `json:"Items"` +} + +// AppParameter Application parameter +type AppParameter struct { + Key string `json:"Key"` + Value string `json:"Value"` +} + +// ApplicationItem encapsulates the embedded model for +// ApplicationItems within the ApplicationItemsPage model +type ApplicationItem struct { + HealthState string `json:"HealthState"` + ID string `json:"Id"` + Name string `json:"Name"` + Parameters []*AppParameter `json:"Parameters"` + Status string `json:"Status"` + TypeName string `json:"TypeName"` + TypeVersion string `json:"TypeVersion"` +} + +// ServiceItemsPage encapsulates the paged response +// model for Services in the Service Fabric API +type ServiceItemsPage struct { + ContinuationToken *string `json:"ContinuationToken"` + Items []ServiceItem `json:"Items"` +} + +// ServiceItem encapsulates the embedded model for +// ServiceItems within the ServiceItemsPage model +type ServiceItem struct { + HasPersistedState bool `json:"HasPersistedState"` + HealthState string `json:"HealthState"` + ID string `json:"Id"` + IsServiceGroup bool `json:"IsServiceGroup"` + ManifestVersion string `json:"ManifestVersion"` + Name string `json:"Name"` + ServiceKind string `json:"ServiceKind"` + ServiceStatus string `json:"ServiceStatus"` + TypeName string `json:"TypeName"` +} + +// PartitionItemsPage encapsulates the paged response +// model for PartitionItems in the Service Fabric API +type PartitionItemsPage struct { + ContinuationToken *string `json:"ContinuationToken"` + Items []PartitionItem `json:"Items"` +} + +// PartitionItem encapsulates the service information +// returned for each PartitionItem under the service +type PartitionItem struct { + CurrentConfigurationEpoch ConfigurationEpoch `json:"CurrentConfigurationEpoch"` + HealthState string `json:"HealthState"` + MinReplicaSetSize int64 `json:"MinReplicaSetSize"` + PartitionInformation PartitionInformation `json:"PartitionInformation"` + PartitionStatus string `json:"PartitionStatus"` + ServiceKind string `json:"ServiceKind"` + TargetReplicaSetSize int64 `json:"TargetReplicaSetSize"` +} + +// ConfigurationEpoch Partition configuration epoch +type ConfigurationEpoch struct { + ConfigurationVersion string `json:"ConfigurationVersion"` + DataLossVersion string `json:"DataLossVersion"` +} + +// PartitionInformation Partition information +type PartitionInformation struct { + HighKey string `json:"HighKey"` + ID string `json:"Id"` + LowKey string `json:"LowKey"` + ServicePartitionKind string `json:"ServicePartitionKind"` +} + +// ReplicaItemBase shared data used +// in both replicas and instances +type ReplicaItemBase struct { + Address string `json:"Address"` + HealthState string `json:"HealthState"` + LastInBuildDurationInSeconds string `json:"LastInBuildDurationInSeconds"` + NodeName string `json:"NodeName"` + ReplicaRole string `json:"ReplicaRole"` + ReplicaStatus string `json:"ReplicaStatus"` + ServiceKind string `json:"ServiceKind"` +} + +// ReplicaItemsPage encapsulates the response +// model for Replicas in the Service Fabric API +type ReplicaItemsPage struct { + ContinuationToken *string `json:"ContinuationToken"` + Items []ReplicaItem `json:"Items"` +} + +// ReplicaItem holds replica specific data +type ReplicaItem struct { + *ReplicaItemBase + ID string `json:"ReplicaId"` +} + +// GetReplicaData returns replica data +func (m *ReplicaItem) GetReplicaData() (string, *ReplicaItemBase) { + return m.ID, m.ReplicaItemBase +} + +// InstanceItemsPage encapsulates the response +// model for Instances in the Service Fabric API +type InstanceItemsPage struct { + ContinuationToken *string `json:"ContinuationToken"` + Items []InstanceItem `json:"Items"` +} + +// InstanceItem hold instance specific data +type InstanceItem struct { + *ReplicaItemBase + ID string `json:"InstanceId"` +} + +// GetReplicaData returns replica data from an instance +func (m *InstanceItem) GetReplicaData() (string, *ReplicaItemBase) { + return m.ID, m.ReplicaItemBase +} + +// ServiceType encapsulates the response model for +// Service types in the Service Fabric API +type ServiceType struct { + ServiceTypeDescription ServiceTypeDescription `json:"ServiceTypeDescription"` + ServiceManifestVersion string `json:"ServiceManifestVersion"` + ServiceManifestName string `json:"ServiceManifestName"` + IsServiceGroup bool `json:"IsServiceGroup"` +} + +// ServiceTypeDescription Service Type Description +type ServiceTypeDescription struct { + IsStateful bool `json:"IsStateful"` + ServiceTypeName string `json:"ServiceTypeName"` + PlacementConstraints string `json:"PlacementConstraints"` + HasPersistedState bool `json:"HasPersistedState"` + Kind string `json:"Kind"` + Extensions []KeyValuePair `json:"Extensions"` + LoadMetrics []interface{} `json:"LoadMetrics"` + ServicePlacementPolicies []interface{} `json:"ServicePlacementPolicies"` +} + +// PropertiesListPage encapsulates the response model for +// PagedPropertyInfoList in the Service Fabric API +type PropertiesListPage struct { + ContinuationToken string `json:"ContinuationToken"` + IsConsistent bool `json:"IsConsistent"` + Properties []Property `json:"Properties"` +} + +// Property Paged Property Info +type Property struct { + Metadata Metadata `json:"Metadata"` + Name string `json:"Name"` + Value PropValue `json:"Value"` +} + +// Metadata Property Metadata +type Metadata struct { + CustomTypeID string `json:"CustomTypeId"` + LastModifiedUtcTimestamp string `json:"LastModifiedUtcTimestamp"` + Parent string `json:"Parent"` + SequenceNumber string `json:"SequenceNumber"` + SizeInBytes int64 `json:"SizeInBytes"` + TypeID string `json:"TypeId"` +} + +// PropValue Property value +type PropValue struct { + Data string `json:"Data"` + Kind string `json:"Kind"` +} + +// KeyValuePair represents a key value pair structure +type KeyValuePair struct { + Key string `json:"Key"` + Value string `json:"Value"` +} + +// ServiceExtensionLabels provides the structure for +// deserialising the XML document used to store labels in an Extension +type ServiceExtensionLabels struct { + XMLName xml.Name `xml:"Labels"` + Label []struct { + XMLName xml.Name `xml:"Label"` + Value string `xml:",chardata"` + Key string `xml:"Key,attr"` + } +}