Add Service Fabric Provider

This commit is contained in:
Lawrence Gripper 2017-11-27 13:26:04 +00:00 committed by Traefiker
parent 9f6f637527
commit 39c1cc1b3c
21 changed files with 1624 additions and 18 deletions

View file

@ -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,

View file

@ -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"`

View file

@ -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
<StatelessServiceType ServiceTypeName="WebServiceType">
<Extensions>
<Extension Name="Traefik">
<Labels xmlns="http://schemas.microsoft.com/2015/03/fabact-no-schema">
<Label Key="traefik.frontend.rule.example2">PathPrefixStrip: /a/path/to/strip</Label>
<Label Key="traefik.expose">true</Label>
<Label Key="traefik.frontend.passHostHeader">true</Label>
</Labels>
</Extension>
</Extensions>
</StatelessServiceType>
```
#### 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.<br>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.<br>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.<br>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 |

12
glide.lock generated
View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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() {

View file

@ -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,
}

View file

@ -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
}

35
vendor/github.com/Masterminds/sprig/regex.go generated vendored Normal file
View file

@ -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)
}

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 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.

View file

@ -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
}

View file

@ -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}}
`

View file

@ -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)
}

View file

@ -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)
}
}
}

View file

@ -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)
}

View file

@ -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

21
vendor/github.com/jjcollinge/servicefabric/LICENSE generated vendored Normal file
View file

@ -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.

20
vendor/github.com/jjcollinge/servicefabric/query.go generated vendored Normal file
View file

@ -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
}

View file

@ -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 <Label key="key">value</Label>
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
}

199
vendor/github.com/jjcollinge/servicefabric/types.go generated vendored Normal file
View file

@ -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"`
}
}