Merge pull request #6847 from hashicorp/pcf-auto-auth

Add PCF auth method, agent, and CLI handler
This commit is contained in:
Becca Petrin 2019-06-19 11:32:03 -07:00 committed by GitHub
commit b965ce035c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 16512 additions and 39 deletions

View File

@ -25,6 +25,7 @@ import (
"github.com/hashicorp/vault/command/agent/auth/gcp"
"github.com/hashicorp/vault/command/agent/auth/jwt"
"github.com/hashicorp/vault/command/agent/auth/kubernetes"
"github.com/hashicorp/vault/command/agent/auth/pcf"
"github.com/hashicorp/vault/command/agent/cache"
"github.com/hashicorp/vault/command/agent/config"
"github.com/hashicorp/vault/command/agent/sink"
@ -342,6 +343,8 @@ func (c *AgentCommand) Run(args []string) int {
method, err = kubernetes.NewKubernetesAuthMethod(authConfig)
case "approle":
method, err = approle.NewApproleAuthMethod(authConfig)
case "pcf":
method, err = pcf.NewPCFAuthMethod(authConfig)
default:
c.UI.Error(fmt.Sprintf("Unknown auth method %q", config.AutoAuth.Method.Type))
return 1

View File

@ -0,0 +1,82 @@
package pcf
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"time"
pcf "github.com/hashicorp/vault-plugin-auth-pcf"
"github.com/hashicorp/vault-plugin-auth-pcf/signatures"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
)
type pcfMethod struct {
mountPath string
roleName string
}
func NewPCFAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
if conf == nil {
return nil, errors.New("empty config")
}
if conf.Config == nil {
return nil, errors.New("empty config data")
}
a := &pcfMethod{
mountPath: conf.MountPath,
}
if raw, ok := conf.Config["role"]; ok {
if roleName, ok := raw.(string); ok {
a.roleName = roleName
} else {
return nil, errors.New("could not convert 'role' config value to string")
}
} else {
return nil, errors.New("missing 'role' value")
}
return a, nil
}
func (p *pcfMethod) Authenticate(ctx context.Context, client *api.Client) (string, map[string]interface{}, error) {
pathToClientCert := os.Getenv(pcf.EnvVarInstanceCertificate)
if pathToClientCert == "" {
return "", nil, fmt.Errorf("missing %q value", pcf.EnvVarInstanceCertificate)
}
certBytes, err := ioutil.ReadFile(pathToClientCert)
if err != nil {
return "", nil, err
}
pathToClientKey := os.Getenv(pcf.EnvVarInstanceKey)
if pathToClientKey == "" {
return "", nil, fmt.Errorf("missing %q value", pcf.EnvVarInstanceKey)
}
signingTime := time.Now().UTC()
signatureData := &signatures.SignatureData{
SigningTime: signingTime,
Role: p.roleName,
CFInstanceCertContents: string(certBytes),
}
signature, err := signatures.Sign(pathToClientKey, signatureData)
if err != nil {
return "", nil, err
}
data := map[string]interface{}{
"role": p.roleName,
"cf_instance_cert": string(certBytes),
"signing_time": signingTime.Format(signatures.TimeFormat),
"signature": signature,
}
return fmt.Sprintf("%s/login", p.mountPath), data, nil
}
func (p *pcfMethod) NewCreds() chan struct{} {
return nil
}
func (p *pcfMethod) CredSuccess() {}
func (p *pcfMethod) Shutdown() {}

View File

@ -0,0 +1,170 @@
package agent
import (
"context"
"io/ioutil"
"os"
"testing"
"time"
hclog "github.com/hashicorp/go-hclog"
log "github.com/hashicorp/go-hclog"
credPCF "github.com/hashicorp/vault-plugin-auth-pcf"
"github.com/hashicorp/vault-plugin-auth-pcf/testing/certificates"
pcfAPI "github.com/hashicorp/vault-plugin-auth-pcf/testing/pcf"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/agent/auth"
agentpcf "github.com/hashicorp/vault/command/agent/auth/pcf"
"github.com/hashicorp/vault/command/agent/sink"
"github.com/hashicorp/vault/command/agent/sink/file"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/helper/logging"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)
func TestPCFEndToEnd(t *testing.T) {
logger := logging.NewVaultLogger(hclog.Trace)
coreConfig := &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: log.NewNullLogger(),
CredentialBackends: map[string]logical.Factory{
"pcf": credPCF.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
cores := cluster.Cores
vault.TestWaitActive(t, cores[0].Core)
client := cores[0].Client
if err := client.Sys().EnableAuthWithOptions("pcf", &api.EnableAuthOptions{
Type: "pcf",
}); err != nil {
t.Fatal(err)
}
testIPAddress := "127.0.0.1"
// Generate some valid certs that look like the ones we get from PCF.
testPCFCerts, err := certificates.Generate(pcfAPI.FoundServiceGUID, pcfAPI.FoundOrgGUID, pcfAPI.FoundSpaceGUID, pcfAPI.FoundAppGUID, testIPAddress)
if err != nil {
t.Fatal(err)
}
defer func() {
if err := testPCFCerts.Close(); err != nil {
t.Fatal(err)
}
}()
// Start a mock server representing their API.
mockPCFAPI := pcfAPI.MockServer(false)
defer mockPCFAPI.Close()
// Configure a CA certificate like a Vault operator would in setting up PCF.
if _, err := client.Logical().Write("auth/pcf/config", map[string]interface{}{
"identity_ca_certificates": testPCFCerts.CACertificate,
"pcf_api_addr": mockPCFAPI.URL,
"pcf_username": pcfAPI.AuthUsername,
"pcf_password": pcfAPI.AuthPassword,
}); err != nil {
t.Fatal(err)
}
// Configure a role to be used for logging in, another thing a Vault operator would do.
if _, err := client.Logical().Write("auth/pcf/roles/test-role", map[string]interface{}{
"bound_instance_ids": pcfAPI.FoundServiceGUID,
"bound_organization_ids": pcfAPI.FoundOrgGUID,
"bound_space_ids": pcfAPI.FoundSpaceGUID,
"bound_application_ids": pcfAPI.FoundAppGUID,
}); err != nil {
t.Fatal(err)
}
os.Setenv(credPCF.EnvVarInstanceCertificate, testPCFCerts.PathToInstanceCertificate)
os.Setenv(credPCF.EnvVarInstanceKey, testPCFCerts.PathToInstanceKey)
ctx, cancelFunc := context.WithCancel(context.Background())
timer := time.AfterFunc(30*time.Second, func() {
cancelFunc()
})
defer timer.Stop()
am, err := agentpcf.NewPCFAuthMethod(&auth.AuthConfig{
MountPath: "auth/pcf",
Config: map[string]interface{}{
"role": "test-role",
},
})
if err != nil {
t.Fatal(err)
}
ahConfig := &auth.AuthHandlerConfig{
Logger: logger.Named("auth.handler"),
Client: client,
}
ah := auth.NewAuthHandler(ahConfig)
go ah.Run(ctx, am)
defer func() {
<-ah.DoneCh
}()
tmpFile, err := ioutil.TempFile("", "auth.tokensink.test.")
if err != nil {
t.Fatal(err)
}
tokenSinkFileName := tmpFile.Name()
tmpFile.Close()
os.Remove(tokenSinkFileName)
t.Logf("output: %s", tokenSinkFileName)
config := &sink.SinkConfig{
Logger: logger.Named("sink.file"),
Config: map[string]interface{}{
"path": tokenSinkFileName,
},
WrapTTL: 10 * time.Second,
}
fs, err := file.NewFileSink(config)
if err != nil {
t.Fatal(err)
}
config.Sink = fs
ss := sink.NewSinkServer(&sink.SinkServerConfig{
Logger: logger.Named("sink.server"),
Client: client,
})
go ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
defer func() {
<-ss.DoneCh
}()
if stat, err := os.Lstat(tokenSinkFileName); err == nil {
t.Fatalf("expected err but got %s", stat)
} else if !os.IsNotExist(err) {
t.Fatal("expected notexist err")
}
// Wait 2 seconds for the env variables to be detected and an auth to be generated.
time.Sleep(time.Second * 2)
token, err := readToken(tokenSinkFileName)
if err != nil {
t.Fatal(err)
}
if token.Token == "" {
t.Fatal("expected token but didn't receive it")
}
}

View File

@ -355,6 +355,7 @@ func TestPredict_Plugins(t *testing.T) {
"nomad",
"oidc",
"okta",
"pcf",
"pki",
"postgresql",
"postgresql-database-plugin",

View File

@ -27,6 +27,7 @@ import (
credCentrify "github.com/hashicorp/vault-plugin-auth-centrify"
credGcp "github.com/hashicorp/vault-plugin-auth-gcp/plugin"
credOIDC "github.com/hashicorp/vault-plugin-auth-jwt"
credPCF "github.com/hashicorp/vault-plugin-auth-pcf"
credAws "github.com/hashicorp/vault/builtin/credential/aws"
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
@ -162,6 +163,7 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
"ldap": &credLdap.CLIHandler{},
"oidc": &credOIDC.CLIHandler{},
"okta": &credOkta.CLIHandler{},
"pcf": &credPCF.CLIHandler{},
"radius": &credUserpass.CLIHandler{
DefaultMount: "radius",
},

3
go.mod
View File

@ -58,7 +58,7 @@ require (
github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-gcp-common v0.5.0
github.com/hashicorp/go-hclog v0.8.0
github.com/hashicorp/go-hclog v0.9.2
github.com/hashicorp/go-memdb v1.0.0
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/go-rootcerts v1.0.0
@ -74,6 +74,7 @@ require (
github.com/hashicorp/vault-plugin-auth-gcp v0.5.1
github.com/hashicorp/vault-plugin-auth-jwt v0.5.1
github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.1
github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c
github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190508211750-4152192cdc0f
github.com/hashicorp/vault-plugin-secrets-ad v0.5.1
github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.1

33
go.sum
View File

@ -2,6 +2,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU=
cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
contrib.go.opencensus.io/exporter/ocagent v0.4.12 h1:jGFvw3l57ViIVEPKKEUXPcLYIXJmQxLUh6ey1eJhwyc=
contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
@ -16,6 +18,8 @@ github.com/DataDog/datadog-go v2.2.0+incompatible h1:V5BKkxACZLjzHjSgBbr2gvLA2Ae
github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Jeffail/gabs v1.1.1 h1:V0uzR08Hj22EX8+8QMhyI9sX2hwRu+/RJhJUmnwda/E=
github.com/Jeffail/gabs v1.1.1/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/Microsoft/go-winio v0.4.12 h1:xAfWHN1IrQ0NJ9TBC0KBZoqLjzDTr1ML+4MywiUOryc=
github.com/Microsoft/go-winio v0.4.12/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
@ -76,10 +80,13 @@ github.com/circonus-labs/circonus-gometrics v2.2.7+incompatible/go.mod h1:nmEj6D
github.com/circonus-labs/circonusllhist v0.1.3 h1:TJH+oke8D16535+jHExHj4nQvzlZrj7ug5D7I/orNUA=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c h1:2zRrJWIt/f9c9HhNHAgrRgq0San5gRRUJTBXLkchal0=
github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808 h1:4BX8f882bXEDKfWIf0wa8HRvpnBoPszJJXL+TVbBw4M=
github.com/containerd/continuity v0.0.0-20181203112020-004b46473808/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y=
github.com/coreos/go-oidc v2.0.0+incompatible h1:+RStIopZ8wooMx+Vs5Bt8zMXxV1ABl5LbakNExNmZIg=
@ -139,6 +146,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-ldap/ldap v3.0.2+incompatible h1:kD5HQcAzlQ7yrhfn+h+MSABeAy/jAJhvIJ/QDllP44g=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -154,6 +162,8 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903 h1:LbsanbbD6LieFkXbj9YNNBupiGHJgFeLpO0j0Fza1h8=
@ -189,6 +199,7 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/googleapis/gax-go/v2 v2.0.4 h1:hU4mGcQI4DaAYW+IbTun+2qEZVFxK0ySjQLTbS0VQKc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY=
@ -224,6 +235,8 @@ github.com/hashicorp/go-gcp-common v0.5.0/go.mod h1:IDGUI2N/OS3PiU4qZcXJeWKPI6O/
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0 h1:z3ollgGRg8RjfJH6UVBaG54R70GFd++QOkvnJH3VSBY=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.0.0 h1:K1O4N2VPndZiTrdH3lmmf5bemr9Xw81KjVwhReIUjTQ=
@ -232,6 +245,8 @@ github.com/hashicorp/go-msgpack v0.5.3 h1:zKjpN5BK/P5lMYrLmBHdBULWbJ0XpYR+7NGzqk
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-plugin v1.0.0 h1:/gQ1sNR8/LHpoxKRQq4PmLBuacfZb4tC93e9B30o/7c=
github.com/hashicorp/go-plugin v1.0.0/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-plugin v1.0.1 h1:4OtAfUGbnKC6yS48p0CtMX2oFYtzFZVv6rok3cRWgnE=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=
@ -274,8 +289,16 @@ github.com/hashicorp/vault-plugin-auth-jwt v0.5.1 h1:d9WLI7oF6VMtwBZwS5bbChc4kW+
github.com/hashicorp/vault-plugin-auth-jwt v0.5.1/go.mod h1:5VU7gc6/BEEFQW/viqMs3LBxI1D1cxJmKqKQEP3JUP4=
github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.1 h1:q6DGb12Vw/CpZ9xDWAmpzxVRKeClFqRFgbIZ3fZcvuY=
github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.1/go.mod h1:qCDsm0njdfUrnN5sFKMLjxGjZKjQf2qB6dReQ4gr4YI=
github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190524170107-2d769dfedad4 h1:dSp8yiXmbfqKfS0j7//fTA+uw3G4OPt7COKDf7oYmMQ=
github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190524170107-2d769dfedad4/go.mod h1:9866PkjxPBXclbEJBKzVGY60pgVIY9b7qZJ5Fa+p6VY=
github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190605234735-619218abcd26 h1:mz5YaAFveImGMooFLAW14kdSBH4jVdRnKTQYAz0fEHw=
github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190605234735-619218abcd26/go.mod h1:9866PkjxPBXclbEJBKzVGY60pgVIY9b7qZJ5Fa+p6VY=
github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c h1:/g4Yr7pCTfKVqjUUVO4/Pkd3Vmw2TB3znuB4lF7ZNNY=
github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c/go.mod h1:AjWJZO3nIHzc1inkx57x5qtIIcpi1sejXiwJVcNpjyc=
github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190508211750-4152192cdc0f h1:BYQVawXauMXQ26I3Pn1Nw9kp/aZD60xmh9ZP3jum0YM=
github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190508211750-4152192cdc0f/go.mod h1:CkOYWfeuC5nAzehBztl94S6VOn2g50h1tffpcNoWCZ8=
github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190617182336-fe4c97e18808 h1:taTbXUW9En/vHp7tVdjhO5XLUmHYxuFJZar+35H7PPg=
github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190617182336-fe4c97e18808/go.mod h1:CkOYWfeuC5nAzehBztl94S6VOn2g50h1tffpcNoWCZ8=
github.com/hashicorp/vault-plugin-secrets-ad v0.5.1 h1:BdiASUZLOvOUs317EnaUNjGxTSw0PYGQA7zJZhDKLC4=
github.com/hashicorp/vault-plugin-secrets-ad v0.5.1/go.mod h1:EH9CI8+0aWRBz8eIgGth0QjttmHWlGvn+8ZmX/ZUetE=
github.com/hashicorp/vault-plugin-secrets-alicloud v0.5.1 h1:72K91p4uLhT/jgtBq2zV5Wn8ocvny4sAN56XOcTxK1w=
@ -317,11 +340,13 @@ github.com/json-iterator/go v0.0.0-20180701071628-ab8a2e0c74be/go.mod h1:+SdeFBv
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f h1:Gsc9mVHLRqBjMgdQCghN9NObCcRncDqxJvBvEaIIQEo=
github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -332,11 +357,14 @@ github.com/kr/pty v1.0.0/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
github.com/marstr/guid v1.1.0 h1:/M4H/1G4avsieL6BbUwCOBzulmoeKVP5ux/3mQNnbyI=
github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho=
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
@ -399,6 +427,7 @@ github.com/openzipkin/zipkin-go v0.1.3/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTm
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
github.com/ory/dockertest v3.3.4+incompatible h1:VrpM6Gqg7CrPm3bL4Wm1skO+zFWLbh7/Xb5kGEbJRh8=
github.com/ory/dockertest v3.3.4+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs=
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c h1:Lgl0gzECD8GnQ5QCWA8o6BtfL6mDH5rQgM4/fX3avOs=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
@ -458,8 +487,10 @@ github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I
github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E=
@ -534,6 +565,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2eP
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
@ -570,6 +602,7 @@ golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxb
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20181219222714-6e267b5cc78e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

View File

@ -11,6 +11,7 @@ import (
credGcp "github.com/hashicorp/vault-plugin-auth-gcp/plugin"
credJWT "github.com/hashicorp/vault-plugin-auth-jwt"
credKube "github.com/hashicorp/vault-plugin-auth-kubernetes"
credPCF "github.com/hashicorp/vault-plugin-auth-pcf"
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
credAws "github.com/hashicorp/vault/builtin/credential/aws"
@ -76,6 +77,7 @@ func newRegistry() *registry {
"ldap": credLdap.Factory,
"oidc": credJWT.Factory,
"okta": credOkta.Factory,
"pcf": credPCF.Factory,
"radius": credRadius.Factory,
"userpass": credUserpass.Factory,
},

202
vendor/code.cloudfoundry.org/gofileutils/LICENSE generated vendored Normal file
View File

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

11
vendor/code.cloudfoundry.org/gofileutils/NOTICE generated vendored Normal file
View File

@ -0,0 +1,11 @@
Copyright (c) 2015-Present CloudFoundry.org Foundation, Inc. All Rights Reserved.
This project contains software that is Copyright (c) 2014-2015 Pivotal Software, Inc.
This project is licensed to you under the Apache License, Version 2.0 (the "License").
You may not use this project except in compliance with the License.
This project may include a number of subcomponents with separate copyright notices
and license terms. Your use of these subcomponents is subject to the terms and
conditions of the subcomponent's license, as noted in the LICENSE file.

View File

@ -0,0 +1,20 @@
package fileutils
import (
"os"
)
func IsDirEmpty(dir string) (isEmpty bool, err error) {
dirFile, err := os.Open(dir)
if err != nil {
return
}
_, readErr := dirFile.Readdirnames(1)
if readErr != nil {
isEmpty = true
} else {
isEmpty = false
}
return
}

View File

@ -0,0 +1,74 @@
package fileutils
import (
"io"
"io/ioutil"
"os"
"path"
"path/filepath"
)
func Open(path string) (file *os.File, err error) {
err = os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm)
if err != nil {
return
}
return os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
}
func Create(path string) (file *os.File, err error) {
err = os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm)
if err != nil {
return
}
return os.Create(path)
}
func CopyPathToPath(fromPath, toPath string) (err error) {
srcFileInfo, err := os.Stat(fromPath)
if err != nil {
return err
}
if srcFileInfo.IsDir() {
err = os.MkdirAll(toPath, srcFileInfo.Mode())
if err != nil {
return err
}
files, err := ioutil.ReadDir(fromPath)
if err != nil {
return err
}
for _, file := range files {
err = CopyPathToPath(path.Join(fromPath, file.Name()), path.Join(toPath, file.Name()))
if err != nil {
return err
}
}
} else {
var dst *os.File
dst, err = Create(toPath)
if err != nil {
return err
}
defer dst.Close()
dst.Chmod(srcFileInfo.Mode())
src, err := os.Open(fromPath)
if err != nil {
return err
}
defer src.Close()
_, err = io.Copy(dst, src)
if err != nil {
return err
}
}
return err
}

View File

@ -0,0 +1,27 @@
package fileutils
import (
"io/ioutil"
"os"
)
func TempDir(namePrefix string, cb func(tmpDir string, err error)) {
tmpDir, err := ioutil.TempDir("", namePrefix)
defer func() {
os.RemoveAll(tmpDir)
}()
cb(tmpDir, err)
}
func TempFile(namePrefix string, cb func(tmpFile *os.File, err error)) {
tmpFile, err := ioutil.TempFile("", namePrefix)
defer func() {
tmpFile.Close()
os.Remove(tmpFile.Name())
}()
cb(tmpFile, err)
}

27
vendor/github.com/Masterminds/semver/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,27 @@
language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- 1.10.x
- tip
# Setting sudo access to false will let Travis CI use containers rather than
# VMs to run the tests. For more details see:
# - http://docs.travis-ci.com/user/workers/container-based-infrastructure/
# - http://docs.travis-ci.com/user/workers/standard-infrastructure/
sudo: false
script:
- make setup
- make test
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/06e3328629952dabe3e0
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: never # options: [always|never|change] default: always

86
vendor/github.com/Masterminds/semver/CHANGELOG.md generated vendored Normal file
View File

@ -0,0 +1,86 @@
# 1.4.2 (2018-04-10)
## Changed
- #72: Updated the docs to point to vert for a console appliaction
- #71: Update the docs on pre-release comparator handling
## Fixed
- #70: Fix the handling of pre-releases and the 0.0.0 release edge case
# 1.4.1 (2018-04-02)
## Fixed
- Fixed #64: Fix pre-release precedence issue (thanks @uudashr)
# 1.4.0 (2017-10-04)
## Changed
- #61: Update NewVersion to parse ints with a 64bit int size (thanks @zknill)
# 1.3.1 (2017-07-10)
## Fixed
- Fixed #57: number comparisons in prerelease sometimes inaccurate
# 1.3.0 (2017-05-02)
## Added
- #45: Added json (un)marshaling support (thanks @mh-cbon)
- Stability marker. See https://masterminds.github.io/stability/
## Fixed
- #51: Fix handling of single digit tilde constraint (thanks @dgodd)
## Changed
- #55: The godoc icon moved from png to svg
# 1.2.3 (2017-04-03)
## Fixed
- #46: Fixed 0.x.x and 0.0.x in constraints being treated as *
# Release 1.2.2 (2016-12-13)
## Fixed
- #34: Fixed issue where hyphen range was not working with pre-release parsing.
# Release 1.2.1 (2016-11-28)
## Fixed
- #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha"
properly.
# Release 1.2.0 (2016-11-04)
## Added
- #20: Added MustParse function for versions (thanks @adamreese)
- #15: Added increment methods on versions (thanks @mh-cbon)
## Fixed
- Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and
might not satisfy the intended compatibility. The change here ignores pre-releases
on constraint checks (e.g., ~ or ^) when a pre-release is not part of the
constraint. For example, `^1.2.3` will ignore pre-releases while
`^1.2.3-alpha` will include them.
# Release 1.1.1 (2016-06-30)
## Changed
- Issue #9: Speed up version comparison performance (thanks @sdboyer)
- Issue #8: Added benchmarks (thanks @sdboyer)
- Updated Go Report Card URL to new location
- Updated Readme to add code snippet formatting (thanks @mh-cbon)
- Updating tagging to v[SemVer] structure for compatibility with other tools.
# Release 1.1.0 (2016-03-11)
- Issue #2: Implemented validation to provide reasons a versions failed a
constraint.
# Release 1.0.1 (2015-12-31)
- Fixed #1: * constraint failing on valid versions.
# Release 1.0.0 (2015-10-20)
- Initial release

20
vendor/github.com/Masterminds/semver/LICENSE.txt generated vendored Normal file
View File

@ -0,0 +1,20 @@
The Masterminds
Copyright (C) 2014-2015, Matt Butcher and Matt Farina
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.

36
vendor/github.com/Masterminds/semver/Makefile generated vendored Normal file
View File

@ -0,0 +1,36 @@
.PHONY: setup
setup:
go get -u gopkg.in/alecthomas/gometalinter.v1
gometalinter.v1 --install
.PHONY: test
test: validate lint
@echo "==> Running tests"
go test -v
.PHONY: validate
validate:
@echo "==> Running static validations"
@gometalinter.v1 \
--disable-all \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable gosimple \
--enable ineffassign \
--enable misspell \
--enable vet \
--tests \
--vendor \
--deadline 60s \
./... || exit_code=1
.PHONY: lint
lint:
@echo "==> Running linters"
@gometalinter.v1 \
--disable-all \
--enable golint \
--vendor \
--deadline 60s \
./... || :

165
vendor/github.com/Masterminds/semver/README.md generated vendored Normal file
View File

@ -0,0 +1,165 @@
# SemVer
The `semver` package provides the ability to work with [Semantic Versions](http://semver.org) in Go. Specifically it provides the ability to:
* Parse semantic versions
* Sort semantic versions
* Check if a semantic version fits within a set of constraints
* Optionally work with a `v` prefix
[![Stability:
Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)
[![Build Status](https://travis-ci.org/Masterminds/semver.svg)](https://travis-ci.org/Masterminds/semver) [![Build status](https://ci.appveyor.com/api/projects/status/jfk66lib7hb985k8/branch/master?svg=true&passingText=windows%20build%20passing&failingText=windows%20build%20failing)](https://ci.appveyor.com/project/mattfarina/semver/branch/master) [![GoDoc](https://godoc.org/github.com/Masterminds/semver?status.svg)](https://godoc.org/github.com/Masterminds/semver) [![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](https://goreportcard.com/report/github.com/Masterminds/semver)
## Parsing Semantic Versions
To parse a semantic version use the `NewVersion` function. For example,
```go
v, err := semver.NewVersion("1.2.3-beta.1+build345")
```
If there is an error the version wasn't parseable. The version object has methods
to get the parts of the version, compare it to other versions, convert the
version back into a string, and get the original string. For more details
please see the [documentation](https://godoc.org/github.com/Masterminds/semver).
## Sorting Semantic Versions
A set of versions can be sorted using the [`sort`](https://golang.org/pkg/sort/)
package from the standard library. For example,
```go
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
v, err := semver.NewVersion(r)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}
vs[i] = v
}
sort.Sort(semver.Collection(vs))
```
## Checking Version Constraints
Checking a version against version constraints is one of the most featureful
parts of the package.
```go
c, err := semver.NewConstraint(">= 1.2.3")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Check if the version meets the constraints. The a variable will be true.
a := c.Check(v)
```
## Basic Comparisons
There are two elements to the comparisons. First, a comparison string is a list
of comma separated and comparisons. These are then separated by || separated or
comparisons. For example, `">= 1.2, < 3.0.0 || >= 4.2.3"` is looking for a
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
greater than or equal to 4.2.3.
The basic comparisons are:
* `=`: equal (aliased to no operator)
* `!=`: not equal
* `>`: greater than
* `<`: less than
* `>=`: greater than or equal to
* `<=`: less than or equal to
_Note, according to the Semantic Version specification pre-releases may not be
API compliant with their release counterpart. It says,_
> _A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version._
_SemVer comparisons without a pre-release value will skip pre-release versions.
For example, `>1.2.3` will skip pre-releases when looking at a list of values
while `>1.2.3-alpha.1` will evaluate pre-releases._
## Hyphen Range Comparisons
There are multiple methods to handle ranges and the first is hyphens ranges.
These look like:
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4, <= 4.5`
## Wildcards In Comparisons
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
for all comparison operators. When used on the `=` operator it falls
back to the pack level comparison (see tilde below). For example,
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `>= 1.2.x` is equivalent to `>= 1.2.0`
* `<= 2.x` is equivalent to `<= 3`
* `*` is equivalent to `>= 0.0.0`
## Tilde Range Comparisons (Patch)
The tilde (`~`) comparison operator is for patch level ranges when a minor
version is specified and major level changes when the minor number is missing.
For example,
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
* `~1` is equivalent to `>= 1, < 2`
* `~2.3` is equivalent to `>= 2.3, < 2.4`
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `~1.x` is equivalent to `>= 1, < 2`
## Caret Range Comparisons (Major)
The caret (`^`) comparison operator is for major level changes. This is useful
when comparisons of API versions as a major change is API breaking. For example,
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
* `^2.3` is equivalent to `>= 2.3, < 3`
* `^2.x` is equivalent to `>= 2.0.0, < 3`
# Validation
In addition to testing a version against a constraint, a version can be validated
against a constraint. When validation fails a slice of errors containing why a
version didn't meet the constraint is returned. For example,
```go
c, err := semver.NewConstraint("<= 1.2.3, >= 1.4")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Validate a version against a constraint.
a, msgs := c.Validate(v)
// a is false
for _, m := range msgs {
fmt.Println(m)
// Loops over the errors which would read
// "1.3 is greater than 1.2.3"
// "1.3 is less than 1.4"
}
```
# Contribute
If you find an issue or want to contribute please file an [issue](https://github.com/Masterminds/semver/issues)
or [create a pull request](https://github.com/Masterminds/semver/pulls).

44
vendor/github.com/Masterminds/semver/appveyor.yml generated vendored Normal file
View File

@ -0,0 +1,44 @@
version: build-{build}.{branch}
clone_folder: C:\gopath\src\github.com\Masterminds\semver
shallow_clone: true
environment:
GOPATH: C:\gopath
platform:
- x64
install:
- go version
- go env
- go get -u gopkg.in/alecthomas/gometalinter.v1
- set PATH=%PATH%;%GOPATH%\bin
- gometalinter.v1.exe --install
build_script:
- go install -v ./...
test_script:
- "gometalinter.v1 \
--disable-all \
--enable deadcode \
--severity deadcode:error \
--enable gofmt \
--enable gosimple \
--enable ineffassign \
--enable misspell \
--enable vet \
--tests \
--vendor \
--deadline 60s \
./... || exit_code=1"
- "gometalinter.v1 \
--disable-all \
--enable golint \
--vendor \
--deadline 60s \
./... || :"
- go test -v
deploy: off

24
vendor/github.com/Masterminds/semver/collection.go generated vendored Normal file
View File

@ -0,0 +1,24 @@
package semver
// Collection is a collection of Version instances and implements the sort
// interface. See the sort package for more details.
// https://golang.org/pkg/sort/
type Collection []*Version
// Len returns the length of a collection. The number of Version instances
// on the slice.
func (c Collection) Len() int {
return len(c)
}
// Less is needed for the sort interface to compare two Version objects on the
// slice. If checks if one is less than the other.
func (c Collection) Less(i, j int) bool {
return c[i].LessThan(c[j])
}
// Swap is needed for the sort interface to replace the Version objects
// at two different positions in the slice.
func (c Collection) Swap(i, j int) {
c[i], c[j] = c[j], c[i]
}

426
vendor/github.com/Masterminds/semver/constraints.go generated vendored Normal file
View File

@ -0,0 +1,426 @@
package semver
import (
"errors"
"fmt"
"regexp"
"strings"
)
// Constraints is one or more constraint that a semantic version can be
// checked against.
type Constraints struct {
constraints [][]*constraint
}
// NewConstraint returns a Constraints instance that a Version instance can
// be checked against. If there is a parse error it will be returned.
func NewConstraint(c string) (*Constraints, error) {
// Rewrite - ranges into a comparison operation.
c = rewriteRange(c)
ors := strings.Split(c, "||")
or := make([][]*constraint, len(ors))
for k, v := range ors {
cs := strings.Split(v, ",")
result := make([]*constraint, len(cs))
for i, s := range cs {
pc, err := parseConstraint(s)
if err != nil {
return nil, err
}
result[i] = pc
}
or[k] = result
}
o := &Constraints{constraints: or}
return o, nil
}
// Check tests if a version satisfies the constraints.
func (cs Constraints) Check(v *Version) bool {
// loop over the ORs and check the inner ANDs
for _, o := range cs.constraints {
joy := true
for _, c := range o {
if !c.check(v) {
joy = false
break
}
}
if joy {
return true
}
}
return false
}
// Validate checks if a version satisfies a constraint. If not a slice of
// reasons for the failure are returned in addition to a bool.
func (cs Constraints) Validate(v *Version) (bool, []error) {
// loop over the ORs and check the inner ANDs
var e []error
for _, o := range cs.constraints {
joy := true
for _, c := range o {
if !c.check(v) {
em := fmt.Errorf(c.msg, v, c.orig)
e = append(e, em)
joy = false
}
}
if joy {
return true, []error{}
}
}
return false, e
}
var constraintOps map[string]cfunc
var constraintMsg map[string]string
var constraintRegex *regexp.Regexp
func init() {
constraintOps = map[string]cfunc{
"": constraintTildeOrEqual,
"=": constraintTildeOrEqual,
"!=": constraintNotEqual,
">": constraintGreaterThan,
"<": constraintLessThan,
">=": constraintGreaterThanEqual,
"=>": constraintGreaterThanEqual,
"<=": constraintLessThanEqual,
"=<": constraintLessThanEqual,
"~": constraintTilde,
"~>": constraintTilde,
"^": constraintCaret,
}
constraintMsg = map[string]string{
"": "%s is not equal to %s",
"=": "%s is not equal to %s",
"!=": "%s is equal to %s",
">": "%s is less than or equal to %s",
"<": "%s is greater than or equal to %s",
">=": "%s is less than %s",
"=>": "%s is less than %s",
"<=": "%s is greater than %s",
"=<": "%s is greater than %s",
"~": "%s does not have same major and minor version as %s",
"~>": "%s does not have same major and minor version as %s",
"^": "%s does not have same major version as %s",
}
ops := make([]string, 0, len(constraintOps))
for k := range constraintOps {
ops = append(ops, regexp.QuoteMeta(k))
}
constraintRegex = regexp.MustCompile(fmt.Sprintf(
`^\s*(%s)\s*(%s)\s*$`,
strings.Join(ops, "|"),
cvRegex))
constraintRangeRegex = regexp.MustCompile(fmt.Sprintf(
`\s*(%s)\s+-\s+(%s)\s*`,
cvRegex, cvRegex))
}
// An individual constraint
type constraint struct {
// The callback function for the restraint. It performs the logic for
// the constraint.
function cfunc
msg string
// The version used in the constraint check. For example, if a constraint
// is '<= 2.0.0' the con a version instance representing 2.0.0.
con *Version
// The original parsed version (e.g., 4.x from != 4.x)
orig string
// When an x is used as part of the version (e.g., 1.x)
minorDirty bool
dirty bool
patchDirty bool
}
// Check if a version meets the constraint
func (c *constraint) check(v *Version) bool {
return c.function(v, c)
}
type cfunc func(v *Version, c *constraint) bool
func parseConstraint(c string) (*constraint, error) {
m := constraintRegex.FindStringSubmatch(c)
if m == nil {
return nil, fmt.Errorf("improper constraint: %s", c)
}
ver := m[2]
orig := ver
minorDirty := false
patchDirty := false
dirty := false
if isX(m[3]) {
ver = "0.0.0"
dirty = true
} else if isX(strings.TrimPrefix(m[4], ".")) || m[4] == "" {
minorDirty = true
dirty = true
ver = fmt.Sprintf("%s.0.0%s", m[3], m[6])
} else if isX(strings.TrimPrefix(m[5], ".")) {
dirty = true
patchDirty = true
ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6])
}
con, err := NewVersion(ver)
if err != nil {
// The constraintRegex should catch any regex parsing errors. So,
// we should never get here.
return nil, errors.New("constraint Parser Error")
}
cs := &constraint{
function: constraintOps[m[1]],
msg: constraintMsg[m[1]],
con: con,
orig: orig,
minorDirty: minorDirty,
patchDirty: patchDirty,
dirty: dirty,
}
return cs, nil
}
// Constraint functions
func constraintNotEqual(v *Version, c *constraint) bool {
if c.dirty {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if c.con.Major() != v.Major() {
return true
}
if c.con.Minor() != v.Minor() && !c.minorDirty {
return true
} else if c.minorDirty {
return false
}
return false
}
return !v.Equal(c.con)
}
func constraintGreaterThan(v *Version, c *constraint) bool {
// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
// exists. This that case.
if !isNonZero(c.con) && isNonZero(v) {
return true
}
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
return v.Compare(c.con) == 1
}
func constraintLessThan(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if !c.dirty {
return v.Compare(c.con) < 0
}
if v.Major() > c.con.Major() {
return false
} else if v.Minor() > c.con.Minor() && !c.minorDirty {
return false
}
return true
}
func constraintGreaterThanEqual(v *Version, c *constraint) bool {
// An edge case the constraint is 0.0.0 and the version is 0.0.0-someprerelease
// exists. This that case.
if !isNonZero(c.con) && isNonZero(v) {
return true
}
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
return v.Compare(c.con) >= 0
}
func constraintLessThanEqual(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if !c.dirty {
return v.Compare(c.con) <= 0
}
if v.Major() > c.con.Major() {
return false
} else if v.Minor() > c.con.Minor() && !c.minorDirty {
return false
}
return true
}
// ~*, ~>* --> >= 0.0.0 (any)
// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0
// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.0
// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0
// ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0
// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0
func constraintTilde(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if v.LessThan(c.con) {
return false
}
// ~0.0.0 is a special case where all constraints are accepted. It's
// equivalent to >= 0.0.0.
if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 &&
!c.minorDirty && !c.patchDirty {
return true
}
if v.Major() != c.con.Major() {
return false
}
if v.Minor() != c.con.Minor() && !c.minorDirty {
return false
}
return true
}
// When there is a .x (dirty) status it automatically opts in to ~. Otherwise
// it's a straight =
func constraintTildeOrEqual(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if c.dirty {
c.msg = constraintMsg["~"]
return constraintTilde(v, c)
}
return v.Equal(c.con)
}
// ^* --> (any)
// ^2, ^2.x, ^2.x.x --> >=2.0.0, <3.0.0
// ^2.0, ^2.0.x --> >=2.0.0, <3.0.0
// ^1.2, ^1.2.x --> >=1.2.0, <2.0.0
// ^1.2.3 --> >=1.2.3, <2.0.0
// ^1.2.0 --> >=1.2.0, <2.0.0
func constraintCaret(v *Version, c *constraint) bool {
// If there is a pre-release on the version but the constraint isn't looking
// for them assume that pre-releases are not compatible. See issue 21 for
// more details.
if v.Prerelease() != "" && c.con.Prerelease() == "" {
return false
}
if v.LessThan(c.con) {
return false
}
if v.Major() != c.con.Major() {
return false
}
return true
}
var constraintRangeRegex *regexp.Regexp
const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
func isX(x string) bool {
switch x {
case "x", "*", "X":
return true
default:
return false
}
}
func rewriteRange(i string) string {
m := constraintRangeRegex.FindAllStringSubmatch(i, -1)
if m == nil {
return i
}
o := i
for _, v := range m {
t := fmt.Sprintf(">= %s, <= %s", v[1], v[11])
o = strings.Replace(o, v[0], t, 1)
}
return o
}
// Detect if a version is not zero (0.0.0)
func isNonZero(v *Version) bool {
if v.Major() != 0 || v.Minor() != 0 || v.Patch() != 0 || v.Prerelease() != "" {
return true
}
return false
}

115
vendor/github.com/Masterminds/semver/doc.go generated vendored Normal file
View File

@ -0,0 +1,115 @@
/*
Package semver provides the ability to work with Semantic Versions (http://semver.org) in Go.
Specifically it provides the ability to:
* Parse semantic versions
* Sort semantic versions
* Check if a semantic version fits within a set of constraints
* Optionally work with a `v` prefix
Parsing Semantic Versions
To parse a semantic version use the `NewVersion` function. For example,
v, err := semver.NewVersion("1.2.3-beta.1+build345")
If there is an error the version wasn't parseable. The version object has methods
to get the parts of the version, compare it to other versions, convert the
version back into a string, and get the original string. For more details
please see the documentation at https://godoc.org/github.com/Masterminds/semver.
Sorting Semantic Versions
A set of versions can be sorted using the `sort` package from the standard library.
For example,
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
vs := make([]*semver.Version, len(raw))
for i, r := range raw {
v, err := semver.NewVersion(r)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}
vs[i] = v
}
sort.Sort(semver.Collection(vs))
Checking Version Constraints
Checking a version against version constraints is one of the most featureful
parts of the package.
c, err := semver.NewConstraint(">= 1.2.3")
if err != nil {
// Handle constraint not being parseable.
}
v, _ := semver.NewVersion("1.3")
if err != nil {
// Handle version not being parseable.
}
// Check if the version meets the constraints. The a variable will be true.
a := c.Check(v)
Basic Comparisons
There are two elements to the comparisons. First, a comparison string is a list
of comma separated and comparisons. These are then separated by || separated or
comparisons. For example, `">= 1.2, < 3.0.0 || >= 4.2.3"` is looking for a
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
greater than or equal to 4.2.3.
The basic comparisons are:
* `=`: equal (aliased to no operator)
* `!=`: not equal
* `>`: greater than
* `<`: less than
* `>=`: greater than or equal to
* `<=`: less than or equal to
Hyphen Range Comparisons
There are multiple methods to handle ranges and the first is hyphens ranges.
These look like:
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4, <= 4.5`
Wildcards In Comparisons
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
for all comparison operators. When used on the `=` operator it falls
back to the pack level comparison (see tilde below). For example,
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `>= 1.2.x` is equivalent to `>= 1.2.0`
* `<= 2.x` is equivalent to `<= 3`
* `*` is equivalent to `>= 0.0.0`
Tilde Range Comparisons (Patch)
The tilde (`~`) comparison operator is for patch level ranges when a minor
version is specified and major level changes when the minor number is missing.
For example,
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
* `~1` is equivalent to `>= 1, < 2`
* `~2.3` is equivalent to `>= 2.3, < 2.4`
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
* `~1.x` is equivalent to `>= 1, < 2`
Caret Range Comparisons (Major)
The caret (`^`) comparison operator is for major level changes. This is useful
when comparisons of API versions as a major change is API breaking. For example,
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
* `^2.3` is equivalent to `>= 2.3, < 3`
* `^2.x` is equivalent to `>= 2.0.0, < 3`
*/
package semver

421
vendor/github.com/Masterminds/semver/version.go generated vendored Normal file
View File

@ -0,0 +1,421 @@
package semver
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"regexp"
"strconv"
"strings"
)
// The compiled version of the regex created at init() is cached here so it
// only needs to be created once.
var versionRegex *regexp.Regexp
var validPrereleaseRegex *regexp.Regexp
var (
// ErrInvalidSemVer is returned a version is found to be invalid when
// being parsed.
ErrInvalidSemVer = errors.New("Invalid Semantic Version")
// ErrInvalidMetadata is returned when the metadata is an invalid format
ErrInvalidMetadata = errors.New("Invalid Metadata string")
// ErrInvalidPrerelease is returned when the pre-release is an invalid format
ErrInvalidPrerelease = errors.New("Invalid Prerelease string")
)
// SemVerRegex is the regular expression used to parse a semantic version.
const SemVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
// ValidPrerelease is the regular expression which validates
// both prerelease and metadata values.
const ValidPrerelease string = `^([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*)`
// Version represents a single semantic version.
type Version struct {
major, minor, patch int64
pre string
metadata string
original string
}
func init() {
versionRegex = regexp.MustCompile("^" + SemVerRegex + "$")
validPrereleaseRegex = regexp.MustCompile(ValidPrerelease)
}
// NewVersion parses a given version and returns an instance of Version or
// an error if unable to parse the version.
func NewVersion(v string) (*Version, error) {
m := versionRegex.FindStringSubmatch(v)
if m == nil {
return nil, ErrInvalidSemVer
}
sv := &Version{
metadata: m[8],
pre: m[5],
original: v,
}
var temp int64
temp, err := strconv.ParseInt(m[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
sv.major = temp
if m[2] != "" {
temp, err = strconv.ParseInt(strings.TrimPrefix(m[2], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
sv.minor = temp
} else {
sv.minor = 0
}
if m[3] != "" {
temp, err = strconv.ParseInt(strings.TrimPrefix(m[3], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("Error parsing version segment: %s", err)
}
sv.patch = temp
} else {
sv.patch = 0
}
return sv, nil
}
// MustParse parses a given version and panics on error.
func MustParse(v string) *Version {
sv, err := NewVersion(v)
if err != nil {
panic(err)
}
return sv
}
// String converts a Version object to a string.
// Note, if the original version contained a leading v this version will not.
// See the Original() method to retrieve the original value. Semantic Versions
// don't contain a leading v per the spec. Instead it's optional on
// impelementation.
func (v *Version) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch)
if v.pre != "" {
fmt.Fprintf(&buf, "-%s", v.pre)
}
if v.metadata != "" {
fmt.Fprintf(&buf, "+%s", v.metadata)
}
return buf.String()
}
// Original returns the original value passed in to be parsed.
func (v *Version) Original() string {
return v.original
}
// Major returns the major version.
func (v *Version) Major() int64 {
return v.major
}
// Minor returns the minor version.
func (v *Version) Minor() int64 {
return v.minor
}
// Patch returns the patch version.
func (v *Version) Patch() int64 {
return v.patch
}
// Prerelease returns the pre-release version.
func (v *Version) Prerelease() string {
return v.pre
}
// Metadata returns the metadata on the version.
func (v *Version) Metadata() string {
return v.metadata
}
// originalVPrefix returns the original 'v' prefix if any.
func (v *Version) originalVPrefix() string {
// Note, only lowercase v is supported as a prefix by the parser.
if v.original != "" && v.original[:1] == "v" {
return v.original[:1]
}
return ""
}
// IncPatch produces the next patch version.
// If the current version does not have prerelease/metadata information,
// it unsets metadata and prerelease values, increments patch number.
// If the current version has any of prerelease or metadata information,
// it unsets both values and keeps curent patch value
func (v Version) IncPatch() Version {
vNext := v
// according to http://semver.org/#spec-item-9
// Pre-release versions have a lower precedence than the associated normal version.
// according to http://semver.org/#spec-item-10
// Build metadata SHOULD be ignored when determining version precedence.
if v.pre != "" {
vNext.metadata = ""
vNext.pre = ""
} else {
vNext.metadata = ""
vNext.pre = ""
vNext.patch = v.patch + 1
}
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
}
// IncMinor produces the next minor version.
// Sets patch to 0.
// Increments minor number.
// Unsets metadata.
// Unsets prerelease status.
func (v Version) IncMinor() Version {
vNext := v
vNext.metadata = ""
vNext.pre = ""
vNext.patch = 0
vNext.minor = v.minor + 1
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
}
// IncMajor produces the next major version.
// Sets patch to 0.
// Sets minor to 0.
// Increments major number.
// Unsets metadata.
// Unsets prerelease status.
func (v Version) IncMajor() Version {
vNext := v
vNext.metadata = ""
vNext.pre = ""
vNext.patch = 0
vNext.minor = 0
vNext.major = v.major + 1
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext
}
// SetPrerelease defines the prerelease value.
// Value must not include the required 'hypen' prefix.
func (v Version) SetPrerelease(prerelease string) (Version, error) {
vNext := v
if len(prerelease) > 0 && !validPrereleaseRegex.MatchString(prerelease) {
return vNext, ErrInvalidPrerelease
}
vNext.pre = prerelease
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext, nil
}
// SetMetadata defines metadata value.
// Value must not include the required 'plus' prefix.
func (v Version) SetMetadata(metadata string) (Version, error) {
vNext := v
if len(metadata) > 0 && !validPrereleaseRegex.MatchString(metadata) {
return vNext, ErrInvalidMetadata
}
vNext.metadata = metadata
vNext.original = v.originalVPrefix() + "" + vNext.String()
return vNext, nil
}
// LessThan tests if one version is less than another one.
func (v *Version) LessThan(o *Version) bool {
return v.Compare(o) < 0
}
// GreaterThan tests if one version is greater than another one.
func (v *Version) GreaterThan(o *Version) bool {
return v.Compare(o) > 0
}
// Equal tests if two versions are equal to each other.
// Note, versions can be equal with different metadata since metadata
// is not considered part of the comparable version.
func (v *Version) Equal(o *Version) bool {
return v.Compare(o) == 0
}
// Compare compares this version to another one. It returns -1, 0, or 1 if
// the version smaller, equal, or larger than the other version.
//
// Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is
// lower than the version without a prerelease.
func (v *Version) Compare(o *Version) int {
// Compare the major, minor, and patch version for differences. If a
// difference is found return the comparison.
if d := compareSegment(v.Major(), o.Major()); d != 0 {
return d
}
if d := compareSegment(v.Minor(), o.Minor()); d != 0 {
return d
}
if d := compareSegment(v.Patch(), o.Patch()); d != 0 {
return d
}
// At this point the major, minor, and patch versions are the same.
ps := v.pre
po := o.Prerelease()
if ps == "" && po == "" {
return 0
}
if ps == "" {
return 1
}
if po == "" {
return -1
}
return comparePrerelease(ps, po)
}
// UnmarshalJSON implements JSON.Unmarshaler interface.
func (v *Version) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
temp, err := NewVersion(s)
if err != nil {
return err
}
v.major = temp.major
v.minor = temp.minor
v.patch = temp.patch
v.pre = temp.pre
v.metadata = temp.metadata
v.original = temp.original
temp = nil
return nil
}
// MarshalJSON implements JSON.Marshaler interface.
func (v *Version) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}
func compareSegment(v, o int64) int {
if v < o {
return -1
}
if v > o {
return 1
}
return 0
}
func comparePrerelease(v, o string) int {
// split the prelease versions by their part. The separator, per the spec,
// is a .
sparts := strings.Split(v, ".")
oparts := strings.Split(o, ".")
// Find the longer length of the parts to know how many loop iterations to
// go through.
slen := len(sparts)
olen := len(oparts)
l := slen
if olen > slen {
l = olen
}
// Iterate over each part of the prereleases to compare the differences.
for i := 0; i < l; i++ {
// Since the lentgh of the parts can be different we need to create
// a placeholder. This is to avoid out of bounds issues.
stemp := ""
if i < slen {
stemp = sparts[i]
}
otemp := ""
if i < olen {
otemp = oparts[i]
}
d := comparePrePart(stemp, otemp)
if d != 0 {
return d
}
}
// Reaching here means two versions are of equal value but have different
// metadata (the part following a +). They are not identical in string form
// but the version comparison finds them to be equal.
return 0
}
func comparePrePart(s, o string) int {
// Fastpath if they are equal
if s == o {
return 0
}
// When s or o are empty we can use the other in an attempt to determine
// the response.
if s == "" {
if o != "" {
return -1
}
return 1
}
if o == "" {
if s != "" {
return 1
}
return -1
}
// When comparing strings "99" is greater than "103". To handle
// cases like this we need to detect numbers and compare them.
oi, n1 := strconv.ParseInt(o, 10, 64)
si, n2 := strconv.ParseInt(s, 10, 64)
// The case where both are strings compare the strings
if n1 != nil && n2 != nil {
if s > o {
return 1
}
return -1
} else if n1 != nil {
// o is a string and s is a number
return -1
} else if n2 != nil {
// s is a string and o is a number
return 1
}
// Both are numbers
if si > oi {
return 1
}
return -1
}

View File

@ -0,0 +1,30 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
_workspace
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
vendor
# GoLand
.idea

View File

@ -0,0 +1,14 @@
language: go
sudo: false
go:
- "1.10"
- "1.11"
script:
- FILES=`find . -iname '*.go' -type f -not -path "./vendor/*"`
# linting
- gofmt -d $FILES
- env GO111MODULE=on go tool vet $FILES
# testing
- go generate
- env GO111MODULE=on go test -v -race

View File

@ -0,0 +1,174 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
name = "github.com/Masterminds/semver"
packages = ["."]
revision = "c7af12943936e8c39859482e61f0574c2fd7fc75"
version = "v1.4.2"
[[projects]]
branch = "master"
name = "github.com/cloudfoundry/gofileutils"
packages = ["fileutils"]
revision = "4d0c80011a0f37da1711c184028bc40137cd45af"
[[projects]]
name = "github.com/codegangsta/inject"
packages = ["."]
revision = "37d7f8432a3e684eef9b2edece76bdfa6ac85b39"
version = "v1.0-rc1"
[[projects]]
name = "github.com/go-martini/martini"
packages = ["."]
revision = "49411a5b646861ad29a6ddd5351717a0a9c49b94"
version = "v1.0"
[[projects]]
branch = "master"
name = "github.com/golang/protobuf"
packages = ["proto"]
revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845"
[[projects]]
branch = "master"
name = "github.com/gopherjs/gopherjs"
packages = ["js"]
revision = "444abdf920945de5d4a977b572bcc6c674d1e4eb"
[[projects]]
name = "github.com/jtolds/gls"
packages = ["."]
revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064"
version = "v4.2.1"
[[projects]]
branch = "master"
name = "github.com/martini-contrib/render"
packages = ["."]
revision = "ec18f8345a1181146728238980606fb1d6f40e8c"
[[projects]]
name = "github.com/onsi/gomega"
packages = [
".",
"format",
"internal/assertion",
"internal/asyncassertion",
"internal/oraclematcher",
"internal/testingtsupport",
"matchers",
"matchers/support/goraph/bipartitegraph",
"matchers/support/goraph/edge",
"matchers/support/goraph/node",
"matchers/support/goraph/util",
"types"
]
revision = "c893efa28eb45626cdaa76c9f653b62488858837"
version = "v1.2.0"
[[projects]]
branch = "master"
name = "github.com/oxtoacart/bpool"
packages = ["."]
revision = "4e1c5567d7c2dd59fa4c7c83d34c2f3528b025d6"
[[projects]]
name = "github.com/pkg/errors"
packages = ["."]
revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
version = "v0.8.0"
[[projects]]
name = "github.com/smartystreets/assertions"
packages = [
".",
"internal/go-render/render",
"internal/oglematchers"
]
revision = "ff1918e1e5a13a74014644ae7c1e0ba2f791364d"
version = "1.8.0"
[[projects]]
name = "github.com/smartystreets/goconvey"
packages = [
"convey",
"convey/gotest",
"convey/reporting"
]
revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
version = "1.6.3"
[[projects]]
branch = "master"
name = "golang.org/x/net"
packages = [
"context",
"context/ctxhttp",
"html",
"html/atom",
"html/charset"
]
revision = "9dfe39835686865bff950a07b394c12a98ddc811"
[[projects]]
branch = "master"
name = "golang.org/x/oauth2"
packages = [
".",
"clientcredentials",
"internal"
]
revision = "f95fa95eaa936d9d87489b15d1d18b97c1ba9c28"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = [
"encoding",
"encoding/charmap",
"encoding/htmlindex",
"encoding/internal",
"encoding/internal/identifier",
"encoding/japanese",
"encoding/korean",
"encoding/simplifiedchinese",
"encoding/traditionalchinese",
"encoding/unicode",
"internal/gen",
"internal/tag",
"internal/utf8internal",
"language",
"runes",
"transform",
"unicode/cldr"
]
revision = "88f656faf3f37f690df1a32515b479415e1a6769"
[[projects]]
name = "google.golang.org/appengine"
packages = [
"internal",
"internal/base",
"internal/datastore",
"internal/log",
"internal/remote_api",
"internal/urlfetch",
"urlfetch"
]
revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
version = "v1.0.0"
[[projects]]
branch = "v2"
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "287cf08546ab5e7e37d55a84f7ed3fd1db036de5"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "2e35689146470eb531e3645c63fb933ad86066e63f57021c20e592e25299a02b"
solver-name = "gps-cdcl"
solver-version = 1

View File

@ -0,0 +1,58 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
name = "github.com/go-martini/martini"
version = "1.0.0"
[[constraint]]
branch = "master"
name = "github.com/martini-contrib/render"
[[constraint]]
name = "github.com/onsi/gomega"
version = "1.2.0"
[[constraint]]
name = "github.com/pkg/errors"
version = "0.8.0"
[[constraint]]
name = "github.com/smartystreets/goconvey"
version = "1.6.3"
[[constraint]]
branch = "master"
name = "golang.org/x/net"
[[constraint]]
branch = "master"
name = "golang.org/x/oauth2"
[[constraint]]
branch = "v2"
name = "gopkg.in/yaml.v2"
[[constraint]]
name = "github.com/Masterminds/semver"
version = "1.4.2"

View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2017 Long Nguyen
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.

View File

@ -0,0 +1,59 @@
# go-cfclient
[![Travis-CI](https://travis-ci.org/cloudfoundry-community/go-cfclient.svg)](https://travis-ci.org/cloudfoundry-community/go-cfclient)
[![GoDoc](https://godoc.org/github.com/cloudfoundry-community/go-cfclient?status.svg)](http://godoc.org/github.com/cloudfoundry-community/go-cfclient)
[![Report card](https://goreportcard.com/badge/github.com/cloudfoundry-community/go-cfclient)](https://goreportcard.com/report/github.com/cloudfoundry-community/go-cfclient)
### Overview
`cfclient` is a package to assist you in writing apps that need to interact with [Cloud Foundry](http://cloudfoundry.org).
It provides functions and structures to retrieve and update
### Usage
```
go get github.com/cloudfoundry-community/go-cfclient
```
NOTE: Currently this project is not versioning its releases and so breaking changes might be introduced.
Whilst hopefully notifications of breaking changes are made via commit messages, ideally your project will use a local
vendoring system to lock in a version of `go-cfclient` that is known to work for you.
This will allow you to control the timing and maintenance of upgrades to newer versions of this library.
Some example code:
```go
package main
import (
"github.com/cloudfoundry-community/go-cfclient"
)
func main() {
c := &cfclient.Config{
ApiAddress: "https://api.10.244.0.34.xip.io",
Username: "admin",
Password: "admin",
}
client, _ := cfclient.NewClient(c)
apps, _ := client.ListApps()
fmt.Println(apps)
}
```
### Development
#### Errors
If the Cloud Foundry error definitions change at <https://github.com/cloudfoundry/cloud_controller_ng/blob/master/vendor/errors/v2.yml>
then the error predicate functions in this package need to be regenerated.
To do this, simply use Go to regenerate the code:
```
go generate
```
### Contributing
Pull requests welcome.

View File

@ -0,0 +1,107 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
type UpdateResponse struct {
Metadata Meta `json:"metadata"`
Entity UpdateResponseEntity `json:"entity"`
}
type AppUpdateResource struct {
Name string `json:"name,omitempty"`
Memory int `json:"memory,omitempty"`
Instances int `json:"instances,omitempty"`
DiskQuota int `json:"disk_quota,omitempty"`
SpaceGuid string `json:"space_guid,omitempty"`
StackGuid string `json:"stack_guid,omitempty"`
State AppState `json:"state,omitempty"`
Command string `json:"command,omitempty"`
Buildpack string `json:"buildpack,omitempty"`
HealthCheckHttpEndpoint string `json:"health_check_http_endpoint,omitempty"`
HealthCheckType string `json:"health_check_type,omitempty"`
HealthCheckTimeout int `json:"health_check_timeout,omitempty"`
Diego bool `json:"diego,omitempty"`
EnableSSH bool `json:"enable_ssh,omitempty"`
DockerImage string `json:"docker_image,omitempty"`
DockerCredentials map[string]interface{} `json:"docker_credentials_json,omitempty"`
Environment map[string]interface{} `json:"environment_json,omitempty"`
StagingFailedReason string `json:"staging_failed_reason,omitempty"`
StagingFailedDescription string `json:"staging_failed_description,omitempty"`
Ports []int `json:"ports,omitempty"`
}
type UpdateResponseEntity struct {
Name string `json:"name"`
Production bool `json:"production"`
SpaceGuid string `json:"space_guid"`
StackGuid string `json:"stack_guid"`
Buildpack string `json:"buildpack"`
DetectedBuildpack string `json:"detected_buildpack"`
DetectedBuildpackGuid string `json:"detected_buildpack_guid"`
Environment map[string]interface{} `json:"environment_json"`
Memory int `json:"memory"`
Instances int `json:"instances"`
DiskQuota int `json:"disk_quota"`
State string `json:"state"`
Version string `json:"version"`
Command string `json:"command"`
Console bool `json:"console"`
Debug string `json:"debug"`
StagingTaskId string `json:"staging_task_id"`
PackageState string `json:"package_state"`
HealthCheckHttpEndpoint string `json:"health_check_http_endpoint"`
HealthCheckType string `json:"health_check_type"`
HealthCheckTimeout int `json:"health_check_timeout"`
StagingFailedReason string `json:"staging_failed_reason"`
StagingFailedDescription string `json:"staging_failed_description"`
Diego bool `json:"diego,omitempty"`
DockerImage string `json:"docker_image"`
DockerCredentials struct {
Username string `json:"username"`
Password string `json:"password"`
} `json:"docker_credentials"`
PackageUpdatedAt string `json:"package_updated_at"`
DetectedStartCommand string `json:"detected_start_command"`
EnableSSH bool `json:"enable_ssh"`
Ports []int `json:"ports"`
SpaceURL string `json:"space_url"`
StackURL string `json:"stack_url"`
RoutesURL string `json:"routes_url"`
EventsURL string `json:"events_url"`
ServiceBindingsUrl string `json:"service_bindings_url"`
RouteMappingsUrl string `json:"route_mappings_url"`
}
func (c *Client) UpdateApp(guid string, aur AppUpdateResource) (UpdateResponse, error) {
var updateResponse UpdateResponse
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(aur)
if err != nil {
return UpdateResponse{}, err
}
req := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/apps/%s", guid), buf)
resp, err := c.DoRequest(req)
if err != nil {
return UpdateResponse{}, err
}
if resp.StatusCode != http.StatusCreated {
return UpdateResponse{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return UpdateResponse{}, err
}
err = json.Unmarshal(body, &updateResponse)
if err != nil {
return UpdateResponse{}, err
}
return updateResponse, nil
}

View File

@ -0,0 +1,80 @@
package cfclient
import (
"encoding/json"
"fmt"
"net/url"
"github.com/pkg/errors"
)
type AppUsageEvent struct {
GUID string `json:"guid"`
CreatedAt string `json:"created_at"`
State string `json:"state"`
PreviousState string `json:"previous_state"`
MemoryInMbPerInstance int `json:"memory_in_mb_per_instance"`
PreviousMemoryInMbPerInstance int `json:"previous_memory_in_mb_per_instance"`
InstanceCount int `json:"instance_count"`
PreviousInstanceCount int `json:"previous_instance_count"`
AppGUID string `json:"app_guid"`
SpaceGUID string `json:"space_guid"`
SpaceName string `json:"space_name"`
OrgGUID string `json:"org_guid"`
BuildpackGUID string `json:"buildpack_guid"`
BuildpackName string `json:"buildpack_name"`
PackageState string `json:"package_state"`
PreviousPackageState string `json:"previous_package_state"`
ParentAppGUID string `json:"parent_app_guid"`
ParentAppName string `json:"parent_app_name"`
ProcessType string `json:"process_type"`
TaskName string `json:"task_name"`
TaskGUID string `json:"task_guid"`
c *Client
}
type AppUsageEventsResponse struct {
TotalResults int `json:"total_results"`
Pages int `json:"total_pages"`
NextURL string `json:"next_url"`
Resources []AppUsageEventResource `json:"resources"`
}
type AppUsageEventResource struct {
Meta Meta `json:"metadata"`
Entity AppUsageEvent `json:"entity"`
}
// ListAppUsageEventsByQuery lists all events matching the provided query.
func (c *Client) ListAppUsageEventsByQuery(query url.Values) ([]AppUsageEvent, error) {
var appUsageEvents []AppUsageEvent
requestURL := fmt.Sprintf("/v2/app_usage_events?%s", query.Encode())
for {
var appUsageEventsResponse AppUsageEventsResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "error requesting events")
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&appUsageEventsResponse); err != nil {
return nil, errors.Wrap(err, "error unmarshaling events")
}
for _, e := range appUsageEventsResponse.Resources {
e.Entity.GUID = e.Meta.Guid
e.Entity.CreatedAt = e.Meta.CreatedAt
e.Entity.c = c
appUsageEvents = append(appUsageEvents, e.Entity)
}
requestURL = appUsageEventsResponse.NextURL
if requestURL == "" {
break
}
}
return appUsageEvents, nil
}
// ListAppUsageEvents lists all unfiltered events.
func (c *Client) ListAppUsageEvents() ([]AppUsageEvent, error) {
return c.ListAppUsageEventsByQuery(nil)
}

View File

@ -0,0 +1,182 @@
package cfclient
import (
"encoding/json"
"io/ioutil"
"time"
"github.com/pkg/errors"
)
const (
//AppCrash app.crash event const
AppCrash = "app.crash"
//AppStart audit.app.start event const
AppStart = "audit.app.start"
//AppStop audit.app.stop event const
AppStop = "audit.app.stop"
//AppUpdate audit.app.update event const
AppUpdate = "audit.app.update"
//AppCreate audit.app.create event const
AppCreate = "audit.app.create"
//AppDelete audit.app.delete-request event const
AppDelete = "audit.app.delete-request"
//AppSSHAuth audit.app.ssh-authorized event const
AppSSHAuth = "audit.app.ssh-authorized"
//AppSSHUnauth audit.app.ssh-unauthorized event const
AppSSHUnauth = "audit.app.ssh-unauthorized"
//AppRestage audit.app.restage event const
AppRestage = "audit.app.restage"
//AppMapRoute audit.app.map-route event const
AppMapRoute = "audit.app.map-route"
//AppUnmapRoute audit.app.unmap-route event const
AppUnmapRoute = "audit.app.unmap-route"
//FilterTimestamp const for query filter timestamp
FilterTimestamp = "timestamp"
//FilterActee const for query filter actee
FilterActee = "actee"
)
//ValidOperators const for all valid operators in a query
var ValidOperators = []string{":", ">=", "<=", "<", ">", "IN"}
// AppEventResponse the entire response
type AppEventResponse struct {
Results int `json:"total_results"`
Pages int `json:"total_pages"`
PrevURL string `json:"prev_url"`
NextURL string `json:"next_url"`
Resources []AppEventResource `json:"resources"`
}
// AppEventResource the event resources
type AppEventResource struct {
Meta Meta `json:"metadata"`
Entity AppEventEntity `json:"entity"`
}
//AppEventQuery a struct for defining queries like 'q=filter>value' or 'q=filter IN a,b,c'
type AppEventQuery struct {
Filter string
Operator string
Value string
}
// The AppEventEntity the actual app event body
type AppEventEntity struct {
//EventTypes are app.crash, audit.app.start, audit.app.stop, audit.app.update, audit.app.create, audit.app.delete-request
EventType string `json:"type"`
//The GUID of the actor.
Actor string `json:"actor"`
//The actor type, user or app
ActorType string `json:"actor_type"`
//The name of the actor.
ActorName string `json:"actor_name"`
//The GUID of the actee.
Actee string `json:"actee"`
//The actee type, space, app or v3-app
ActeeType string `json:"actee_type"`
//The name of the actee.
ActeeName string `json:"actee_name"`
//Timestamp format "2016-02-26T13:29:44Z". The event creation time.
Timestamp time.Time `json:"timestamp"`
MetaData struct {
//app.crash event fields
ExitDescription string `json:"exit_description,omitempty"`
ExitReason string `json:"reason,omitempty"`
ExitStatus string `json:"exit_status,omitempty"`
Request struct {
Name string `json:"name,omitempty"`
Instances float64 `json:"instances,omitempty"`
State string `json:"state,omitempty"`
Memory float64 `json:"memory,omitempty"`
EnvironmentVars string `json:"environment_json,omitempty"`
DockerCredentials string `json:"docker_credentials_json,omitempty"`
//audit.app.create event fields
Console bool `json:"console,omitempty"`
Buildpack string `json:"buildpack,omitempty"`
Space string `json:"space_guid,omitempty"`
HealthcheckType string `json:"health_check_type,omitempty"`
HealthcheckTimeout float64 `json:"health_check_timeout,omitempty"`
Production bool `json:"production,omitempty"`
//app.crash event fields
Index float64 `json:"index,omitempty"`
} `json:"request"`
} `json:"metadata"`
}
// ListAppEvents returns all app events based on eventType
func (c *Client) ListAppEvents(eventType string) ([]AppEventEntity, error) {
return c.ListAppEventsByQuery(eventType, nil)
}
// ListAppEventsByQuery returns all app events based on eventType and queries
func (c *Client) ListAppEventsByQuery(eventType string, queries []AppEventQuery) ([]AppEventEntity, error) {
var events []AppEventEntity
if eventType != AppCrash && eventType != AppStart && eventType != AppStop && eventType != AppUpdate && eventType != AppCreate &&
eventType != AppDelete && eventType != AppSSHAuth && eventType != AppSSHUnauth && eventType != AppRestage &&
eventType != AppMapRoute && eventType != AppUnmapRoute {
return nil, errors.New("Unsupported app event type " + eventType)
}
var query = "/v2/events?q=type:" + eventType
//adding the additional queries
if queries != nil && len(queries) > 0 {
for _, eventQuery := range queries {
if eventQuery.Filter != FilterTimestamp && eventQuery.Filter != FilterActee {
return nil, errors.New("Unsupported query filter type " + eventQuery.Filter)
}
if !stringInSlice(eventQuery.Operator, ValidOperators) {
return nil, errors.New("Unsupported query operator type " + eventQuery.Operator)
}
query += "&q=" + eventQuery.Filter + eventQuery.Operator + eventQuery.Value
}
}
for {
eventResponse, err := c.getAppEventsResponse(query)
if err != nil {
return []AppEventEntity{}, err
}
for _, event := range eventResponse.Resources {
events = append(events, event.Entity)
}
query = eventResponse.NextURL
if query == "" {
break
}
}
return events, nil
}
func (c *Client) getAppEventsResponse(query string) (AppEventResponse, error) {
var eventResponse AppEventResponse
r := c.NewRequest("GET", query)
resp, err := c.DoRequest(r)
if err != nil {
return AppEventResponse{}, errors.Wrap(err, "Error requesting appevents")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return AppEventResponse{}, errors.Wrap(err, "Error reading appevents response body")
}
err = json.Unmarshal(resBody, &eventResponse)
if err != nil {
return AppEventResponse{}, errors.Wrap(err, "Error unmarshalling appevent")
}
return eventResponse, nil
}
func stringInSlice(str string, list []string) bool {
for _, v := range list {
if v == str {
return true
}
}
return false
}

View File

@ -0,0 +1,659 @@
package cfclient
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/pkg/errors"
)
type AppResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []AppResource `json:"resources"`
}
type AppResource struct {
Meta Meta `json:"metadata"`
Entity App `json:"entity"`
}
type AppState string
const (
APP_STOPPED AppState = "STOPPED"
APP_STARTED AppState = "STARTED"
)
type HealthCheckType string
const (
HEALTH_HTTP HealthCheckType = "http"
HEALTH_PORT HealthCheckType = "port"
HEALTH_PROCESS HealthCheckType = "process"
)
type DockerCredentials struct {
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
}
type AppCreateRequest struct {
Name string `json:"name"`
SpaceGuid string `json:"space_guid"`
// Memory for the app, in MB
Memory int `json:"memory,omitempty"`
// Instances to startup
Instances int `json:"instances,omitempty"`
// Disk quota in MB
DiskQuota int `json:"disk_quota,omitempty"`
StackGuid string `json:"stack_guid,omitempty"`
// Desired state of the app. Either "STOPPED" or "STARTED"
State AppState `json:"state,omitempty"`
// Command to start an app
Command string `json:"command,omitempty"`
// Buildpack to build the app. Three options:
// 1. Blank for autodetection
// 2. GIT url
// 3. Name of an installed buildpack
Buildpack string `json:"buildpack,omitempty"`
// Endpoint to check if an app is healthy
HealthCheckHttpEndpoint string `json:"health_check_http_endpoint,omitempty"`
// How to check if an app is healthy. Defaults to HEALTH_PORT if not specified
HealthCheckType HealthCheckType `json:"health_check_type,omitempty"`
Diego bool `json:"diego,omitempty"`
EnableSSH bool `json:"enable_ssh,omitempty"`
DockerImage string `json:"docker_image,omitempty"`
DockerCredentials DockerCredentials `json:"docker_credentials,omitempty"`
Environment map[string]interface{} `json:"environment_json,omitempty"`
}
type App struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Name string `json:"name"`
Memory int `json:"memory"`
Instances int `json:"instances"`
DiskQuota int `json:"disk_quota"`
SpaceGuid string `json:"space_guid"`
StackGuid string `json:"stack_guid"`
State string `json:"state"`
PackageState string `json:"package_state"`
Command string `json:"command"`
Buildpack string `json:"buildpack"`
DetectedBuildpack string `json:"detected_buildpack"`
DetectedBuildpackGuid string `json:"detected_buildpack_guid"`
HealthCheckHttpEndpoint string `json:"health_check_http_endpoint"`
HealthCheckType string `json:"health_check_type"`
HealthCheckTimeout int `json:"health_check_timeout"`
Diego bool `json:"diego"`
EnableSSH bool `json:"enable_ssh"`
DetectedStartCommand string `json:"detected_start_command"`
DockerImage string `json:"docker_image"`
DockerCredentials map[string]interface{} `json:"docker_credentials_json"`
Environment map[string]interface{} `json:"environment_json"`
StagingFailedReason string `json:"staging_failed_reason"`
StagingFailedDescription string `json:"staging_failed_description"`
Ports []int `json:"ports"`
SpaceURL string `json:"space_url"`
SpaceData SpaceResource `json:"space"`
PackageUpdatedAt string `json:"package_updated_at"`
c *Client
}
type AppInstance struct {
State string `json:"state"`
Since sinceTime `json:"since"`
}
type AppStats struct {
State string `json:"state"`
Stats struct {
Name string `json:"name"`
Uris []string `json:"uris"`
Host string `json:"host"`
Port int `json:"port"`
Uptime int `json:"uptime"`
MemQuota int `json:"mem_quota"`
DiskQuota int `json:"disk_quota"`
FdsQuota int `json:"fds_quota"`
Usage struct {
Time statTime `json:"time"`
CPU float64 `json:"cpu"`
Mem int `json:"mem"`
Disk int `json:"disk"`
} `json:"usage"`
} `json:"stats"`
}
type AppSummary struct {
Guid string `json:"guid"`
Name string `json:"name"`
ServiceCount int `json:"service_count"`
RunningInstances int `json:"running_instances"`
SpaceGuid string `json:"space_guid"`
StackGuid string `json:"stack_guid"`
Buildpack string `json:"buildpack"`
DetectedBuildpack string `json:"detected_buildpack"`
Environment map[string]interface{} `json:"environment_json"`
Memory int `json:"memory"`
Instances int `json:"instances"`
DiskQuota int `json:"disk_quota"`
State string `json:"state"`
Command string `json:"command"`
PackageState string `json:"package_state"`
HealthCheckType string `json:"health_check_type"`
HealthCheckTimeout int `json:"health_check_timeout"`
StagingFailedReason string `json:"staging_failed_reason"`
StagingFailedDescription string `json:"staging_failed_description"`
Diego bool `json:"diego"`
DockerImage string `json:"docker_image"`
DetectedStartCommand string `json:"detected_start_command"`
EnableSSH bool `json:"enable_ssh"`
DockerCredentials map[string]interface{} `json:"docker_credentials_json"`
}
type AppEnv struct {
// These can have arbitrary JSON so need to map to interface{}
Environment map[string]interface{} `json:"environment_json"`
StagingEnv map[string]interface{} `json:"staging_env_json"`
RunningEnv map[string]interface{} `json:"running_env_json"`
SystemEnv map[string]interface{} `json:"system_env_json"`
ApplicationEnv map[string]interface{} `json:"application_env_json"`
}
// Custom time types to handle non-RFC3339 formatting in API JSON
type sinceTime struct {
time.Time
}
func (s *sinceTime) UnmarshalJSON(b []byte) (err error) {
timeFlt, err := strconv.ParseFloat(string(b), 64)
if err != nil {
return err
}
time := time.Unix(int64(timeFlt), 0)
*s = sinceTime{time}
return nil
}
func (s sinceTime) ToTime() time.Time {
t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate))
return t
}
type statTime struct {
time.Time
}
func (s *statTime) UnmarshalJSON(b []byte) (err error) {
timeString, err := strconv.Unquote(string(b))
if err != nil {
return err
}
possibleFormats := [...]string{time.RFC3339, time.RFC3339Nano, "2006-01-02 15:04:05 -0700", "2006-01-02 15:04:05 MST"}
var value time.Time
for _, possibleFormat := range possibleFormats {
if value, err = time.Parse(possibleFormat, timeString); err == nil {
*s = statTime{value}
return nil
}
}
return fmt.Errorf("%s was not in any of the expected Date Formats %v", timeString, possibleFormats)
}
func (s statTime) ToTime() time.Time {
t, _ := time.Parse(time.UnixDate, s.Format(time.UnixDate))
return t
}
func (a *App) Space() (Space, error) {
var spaceResource SpaceResource
r := a.c.NewRequest("GET", a.SpaceURL)
resp, err := a.c.DoRequest(r)
if err != nil {
return Space{}, errors.Wrap(err, "Error requesting space")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Space{}, errors.Wrap(err, "Error reading space response")
}
err = json.Unmarshal(resBody, &spaceResource)
if err != nil {
return Space{}, errors.Wrap(err, "Error unmarshalling body")
}
return a.c.mergeSpaceResource(spaceResource), nil
}
func (a *App) Summary() (AppSummary, error) {
var appSummary AppSummary
requestUrl := fmt.Sprintf("/v2/apps/%s/summary", a.Guid)
r := a.c.NewRequest("GET", requestUrl)
resp, err := a.c.DoRequest(r)
if err != nil {
return AppSummary{}, errors.Wrap(err, "Error requesting app summary")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return AppSummary{}, errors.Wrap(err, "Error reading app summary body")
}
err = json.Unmarshal(resBody, &appSummary)
if err != nil {
return AppSummary{}, errors.Wrap(err, "Error unmarshalling app summary")
}
return appSummary, nil
}
// ListAppsByQueryWithLimits queries totalPages app info. When totalPages is
// less and equal than 0, it queries all app info
// When there are no more than totalPages apps on server side, all apps info will be returned
func (c *Client) ListAppsByQueryWithLimits(query url.Values, totalPages int) ([]App, error) {
return c.listApps("/v2/apps?"+query.Encode(), totalPages)
}
func (c *Client) ListAppsByQuery(query url.Values) ([]App, error) {
return c.listApps("/v2/apps?"+query.Encode(), -1)
}
// GetAppByGuidNoInlineCall will fetch app info including space and orgs information
// Without using inline-relations-depth=2 call
func (c *Client) GetAppByGuidNoInlineCall(guid string) (App, error) {
var appResource AppResource
r := c.NewRequest("GET", "/v2/apps/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return App{}, errors.Wrap(err, "Error requesting apps")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return App{}, errors.Wrap(err, "Error reading app response body")
}
err = json.Unmarshal(resBody, &appResource)
if err != nil {
return App{}, errors.Wrap(err, "Error unmarshalling app")
}
app := c.mergeAppResource(appResource)
// If no Space Information no need to check org.
if app.SpaceGuid != "" {
//Getting Spaces Resource
space, err := app.Space()
if err != nil {
errors.Wrap(err, "Unable to get the Space for the apps "+app.Name)
} else {
app.SpaceData.Entity = space
}
//Getting orgResource
org, err := app.SpaceData.Entity.Org()
if err != nil {
errors.Wrap(err, "Unable to get the Org for the apps "+app.Name)
} else {
app.SpaceData.Entity.OrgData.Entity = org
}
}
return app, nil
}
func (c *Client) ListApps() ([]App, error) {
q := url.Values{}
q.Set("inline-relations-depth", "2")
return c.ListAppsByQuery(q)
}
func (c *Client) ListAppsByRoute(routeGuid string) ([]App, error) {
return c.listApps(fmt.Sprintf("/v2/routes/%s/apps", routeGuid), -1)
}
func (c *Client) listApps(requestUrl string, totalPages int) ([]App, error) {
pages := 0
apps := []App{}
for {
var appResp AppResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting apps")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading app request")
}
err = json.Unmarshal(resBody, &appResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling app")
}
for _, app := range appResp.Resources {
apps = append(apps, c.mergeAppResource(app))
}
requestUrl = appResp.NextUrl
if requestUrl == "" {
break
}
pages += 1
if totalPages > 0 && pages >= totalPages {
break
}
}
return apps, nil
}
func (c *Client) GetAppInstances(guid string) (map[string]AppInstance, error) {
var appInstances map[string]AppInstance
requestURL := fmt.Sprintf("/v2/apps/%s/instances", guid)
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting app instances")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading app instances")
}
err = json.Unmarshal(resBody, &appInstances)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling app instances")
}
return appInstances, nil
}
func (c *Client) GetAppEnv(guid string) (AppEnv, error) {
var appEnv AppEnv
requestURL := fmt.Sprintf("/v2/apps/%s/env", guid)
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return appEnv, errors.Wrap(err, "Error requesting app env")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return appEnv, errors.Wrap(err, "Error reading app env")
}
err = json.Unmarshal(resBody, &appEnv)
if err != nil {
return appEnv, errors.Wrap(err, "Error unmarshalling app env")
}
return appEnv, nil
}
func (c *Client) GetAppRoutes(guid string) ([]Route, error) {
return c.fetchRoutes(fmt.Sprintf("/v2/apps/%s/routes", guid))
}
func (c *Client) GetAppStats(guid string) (map[string]AppStats, error) {
var appStats map[string]AppStats
requestURL := fmt.Sprintf("/v2/apps/%s/stats", guid)
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting app stats")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading app stats")
}
err = json.Unmarshal(resBody, &appStats)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling app stats")
}
return appStats, nil
}
func (c *Client) KillAppInstance(guid string, index string) error {
requestURL := fmt.Sprintf("/v2/apps/%s/instances/%s", guid, index)
r := c.NewRequest("DELETE", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index)
}
defer resp.Body.Close()
if resp.StatusCode != 204 {
return errors.Wrapf(err, "Error stopping app %s at index %s", guid, index)
}
return nil
}
func (c *Client) GetAppByGuid(guid string) (App, error) {
var appResource AppResource
r := c.NewRequest("GET", "/v2/apps/"+guid+"?inline-relations-depth=2")
resp, err := c.DoRequest(r)
if err != nil {
return App{}, errors.Wrap(err, "Error requesting apps")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return App{}, errors.Wrap(err, "Error reading app response body")
}
err = json.Unmarshal(resBody, &appResource)
if err != nil {
return App{}, errors.Wrap(err, "Error unmarshalling app")
}
return c.mergeAppResource(appResource), nil
}
func (c *Client) AppByGuid(guid string) (App, error) {
return c.GetAppByGuid(guid)
}
//AppByName takes an appName, and GUIDs for a space and org, and performs
// the API lookup with those query parameters set to return you the desired
// App object.
func (c *Client) AppByName(appName, spaceGuid, orgGuid string) (app App, err error) {
query := url.Values{}
query.Add("q", fmt.Sprintf("organization_guid:%s", orgGuid))
query.Add("q", fmt.Sprintf("space_guid:%s", spaceGuid))
query.Add("q", fmt.Sprintf("name:%s", appName))
apps, err := c.ListAppsByQuery(query)
if err != nil {
return
}
if len(apps) == 0 {
err = fmt.Errorf("No app found with name: `%s` in space with GUID `%s` and org with GUID `%s`", appName, spaceGuid, orgGuid)
return
}
app = apps[0]
return
}
// UploadAppBits uploads the application's contents
func (c *Client) UploadAppBits(file io.Reader, appGUID string) error {
requestFile, err := ioutil.TempFile("", "requests")
defer func() {
requestFile.Close()
os.Remove(requestFile.Name())
}()
writer := multipart.NewWriter(requestFile)
err = writer.WriteField("resources", "[]")
if err != nil {
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
}
part, err := writer.CreateFormFile("application", "application.zip")
if err != nil {
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
}
_, err = io.Copy(part, file)
if err != nil {
return errors.Wrapf(err, "Error uploading app %s bits, failed to copy all bytes", appGUID)
}
err = writer.Close()
if err != nil {
return errors.Wrapf(err, "Error uploading app %s bits, failed to close multipart writer", appGUID)
}
requestFile.Seek(0, 0)
fileStats, err := requestFile.Stat()
if err != nil {
return errors.Wrapf(err, "Error uploading app %s bits, failed to get temp file stats", appGUID)
}
requestURL := fmt.Sprintf("/v2/apps/%s/bits", appGUID)
r := c.NewRequestWithBody("PUT", requestURL, requestFile)
req, err := r.toHTTP()
if err != nil {
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
}
req.ContentLength = fileStats.Size()
contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())
req.Header.Set("Content-Type", contentType)
resp, err := c.Do(req)
if err != nil {
return errors.Wrapf(err, "Error uploading app %s bits", appGUID)
}
if resp.StatusCode != http.StatusCreated {
return errors.Wrapf(err, "Error uploading app %s bits, response code: %d", appGUID, resp.StatusCode)
}
return nil
}
// GetAppBits downloads the application's bits as a tar file
func (c *Client) GetAppBits(guid string) (io.ReadCloser, error) {
requestURL := fmt.Sprintf("/v2/apps/%s/download", guid)
req := c.NewRequest("GET", requestURL)
resp, err := c.DoRequestWithoutRedirects(req)
if err != nil {
return nil, errors.Wrapf(err, "Error downloading app %s bits, API request failed", guid)
}
if isResponseRedirect(resp) {
// directly download the bits from blobstore using a non cloud controller transport
// some blobstores will return a 400 if an Authorization header is sent
blobStoreLocation := resp.Header.Get("Location")
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Config.SkipSslValidation},
}
client := &http.Client{Transport: tr}
resp, err = client.Get(blobStoreLocation)
if err != nil {
return nil, errors.Wrapf(err, "Error downloading app %s bits from blobstore", guid)
}
} else {
return nil, errors.Wrapf(err, "Error downloading app %s bits, expected redirect to blobstore", guid)
}
return resp.Body, nil
}
// CreateApp creates a new empty application that still needs it's
// app bit uploaded and to be started
func (c *Client) CreateApp(req AppCreateRequest) (App, error) {
var appResp AppResource
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return App{}, err
}
r := c.NewRequestWithBody("POST", "/v2/apps", buf)
resp, err := c.DoRequest(r)
if err != nil {
return App{}, errors.Wrapf(err, "Error creating app %s", req.Name)
}
if resp.StatusCode != http.StatusCreated {
return App{}, errors.Wrapf(err, "Error creating app %s, response code: %d", req.Name, resp.StatusCode)
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return App{}, errors.Wrapf(err, "Error reading app %s http response body", req.Name)
}
err = json.Unmarshal(resBody, &appResp)
if err != nil {
return App{}, errors.Wrapf(err, "Error deserializing app %s response", req.Name)
}
return c.mergeAppResource(appResp), nil
}
func (c *Client) StartApp(guid string) error {
startRequest := strings.NewReader(`{ "state": "STARTED" }`)
resp, err := c.DoRequest(c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/apps/%s", guid), startRequest))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error starting app %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) StopApp(guid string) error {
stopRequest := strings.NewReader(`{ "state": "STOPPED" }`)
resp, err := c.DoRequest(c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/apps/%s", guid), stopRequest))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error stopping app %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) DeleteApp(guid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/apps/%s", guid)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting app %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) mergeAppResource(app AppResource) App {
app.Entity.Guid = app.Meta.Guid
app.Entity.CreatedAt = app.Meta.CreatedAt
app.Entity.UpdatedAt = app.Meta.UpdatedAt
app.Entity.SpaceData.Entity.Guid = app.Entity.SpaceData.Meta.Guid
app.Entity.SpaceData.Entity.OrgData.Entity.Guid = app.Entity.SpaceData.Entity.OrgData.Meta.Guid
app.Entity.c = c
return app.Entity
}
func isResponseRedirect(res *http.Response) bool {
switch res.StatusCode {
case http.StatusTemporaryRedirect, http.StatusPermanentRedirect, http.StatusMovedPermanently, http.StatusFound, http.StatusSeeOther:
return true
}
return false
}

View File

@ -0,0 +1,247 @@
package cfclient
import (
"encoding/json"
"io"
"io/ioutil"
"mime/multipart"
"os"
"fmt"
"net/http"
"code.cloudfoundry.org/gofileutils/fileutils"
"github.com/pkg/errors"
)
type BuildpackResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []BuildpackResource `json:"resources"`
}
type BuildpackResource struct {
Meta Meta `json:"metadata"`
Entity Buildpack `json:"entity"`
}
type Buildpack struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Locked bool `json:"locked"`
Position int `json:"position"`
Filename string `json:"filename"`
Stack string `json:"stack"`
c *Client
}
type BuildpackRequest struct {
// These are all pointers to the values so that we can tell
// whether people wanted position 0, or enable/unlock values,
// vs whether they didn't specify them and want them unchanged/default.
Name *string `json:"name,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
Locked *bool `json:"locked,omitempty"`
Position *int `json:"position,omitempty"`
Stack *string `json:"stack,omitempty"`
}
func (c *Client) CreateBuildpack(bpr *BuildpackRequest) (*Buildpack, error) {
if bpr.Name == nil || *bpr.Name == "" {
return nil, errors.New("Unable to create a buidlpack with no name")
}
requestUrl := "/v2/buildpacks"
req := c.NewRequest("POST", requestUrl)
req.obj = bpr
resp, err := c.DoRequest(req)
if err != nil {
return nil, errors.Wrap(err, "Error creating buildpack:")
}
bp, err := c.handleBuildpackResp(resp)
if err != nil {
return nil, errors.Wrap(err, "Error creating buildpack:")
}
return &bp, nil
}
func (c *Client) ListBuildpacks() ([]Buildpack, error) {
var buildpacks []Buildpack
requestUrl := "/v2/buildpacks"
for {
buildpackResp, err := c.getBuildpackResponse(requestUrl)
if err != nil {
return []Buildpack{}, err
}
for _, buildpack := range buildpackResp.Resources {
buildpacks = append(buildpacks, c.mergeBuildpackResource(buildpack))
}
requestUrl = buildpackResp.NextUrl
if requestUrl == "" {
break
}
}
return buildpacks, nil
}
func (c *Client) DeleteBuildpack(guid string, async bool) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/buildpacks/%s?async=%t", guid, async)))
if err != nil {
return err
}
if (async && (resp.StatusCode != http.StatusAccepted)) || (!async && (resp.StatusCode != http.StatusNoContent)) {
return errors.Wrapf(err, "Error deleting buildpack %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) getBuildpackResponse(requestUrl string) (BuildpackResponse, error) {
var buildpackResp BuildpackResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return BuildpackResponse{}, errors.Wrap(err, "Error requesting buildpacks")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return BuildpackResponse{}, errors.Wrap(err, "Error reading buildpack request")
}
err = json.Unmarshal(resBody, &buildpackResp)
if err != nil {
return BuildpackResponse{}, errors.Wrap(err, "Error unmarshalling buildpack")
}
return buildpackResp, nil
}
func (c *Client) mergeBuildpackResource(buildpack BuildpackResource) Buildpack {
buildpack.Entity.Guid = buildpack.Meta.Guid
buildpack.Entity.CreatedAt = buildpack.Meta.CreatedAt
buildpack.Entity.UpdatedAt = buildpack.Meta.UpdatedAt
buildpack.Entity.c = c
return buildpack.Entity
}
func (c *Client) GetBuildpackByGuid(buildpackGUID string) (Buildpack, error) {
requestUrl := fmt.Sprintf("/v2/buildpacks/%s", buildpackGUID)
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return Buildpack{}, errors.Wrap(err, "Error requesting buildpack info")
}
return c.handleBuildpackResp(resp)
}
func (c *Client) handleBuildpackResp(resp *http.Response) (Buildpack, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return Buildpack{}, err
}
var buildpackResource BuildpackResource
if err := json.Unmarshal(body, &buildpackResource); err != nil {
return Buildpack{}, err
}
return c.mergeBuildpackResource(buildpackResource), nil
}
func (b *Buildpack) Upload(file io.Reader, fileName string) error {
var capturedErr error
fileutils.TempFile("requests", func(requestFile *os.File, err error) {
if err != nil {
capturedErr = err
return
}
writer := multipart.NewWriter(requestFile)
part, err := writer.CreateFormFile("buildpack", fileName)
if err != nil {
_ = writer.Close()
capturedErr = err
return
}
_, err = io.Copy(part, file)
if err != nil {
capturedErr = fmt.Errorf("Error creating upload: %s", err.Error())
return
}
err = writer.Close()
if err != nil {
capturedErr = err
return
}
requestFile.Seek(0, 0)
fileStats, err := requestFile.Stat()
if err != nil {
capturedErr = fmt.Errorf("Error getting file info: %s", err)
}
req, err := http.NewRequest("PUT", fmt.Sprintf("%s/v2/buildpacks/%s/bits", b.c.Config.ApiAddress, b.Guid), requestFile)
if err != nil {
capturedErr = err
return
}
req.ContentLength = fileStats.Size()
contentType := fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())
req.Header.Set("Content-Type", contentType)
resp, err := b.c.Do(req) //client.Do() handles the HTTP status code checking for us
if err != nil {
capturedErr = err
return
}
defer resp.Body.Close()
})
return errors.Wrap(capturedErr, "Error uploading buildpack:")
}
func (b *Buildpack) Update(bpr *BuildpackRequest) error {
requestUrl := fmt.Sprintf("/v2/buildpacks/%s", b.Guid)
req := b.c.NewRequest("PUT", requestUrl)
req.obj = bpr
resp, err := b.c.DoRequest(req)
if err != nil {
return errors.Wrap(err, "Error updating buildpack:")
}
newBp, err := b.c.handleBuildpackResp(resp)
if err != nil {
return errors.Wrap(err, "Error updating buildpack:")
}
b.Name = newBp.Name
b.Locked = newBp.Locked
b.Enabled = newBp.Enabled
return nil
}
func (bpr *BuildpackRequest) Lock() {
b := true
bpr.Locked = &b
}
func (bpr *BuildpackRequest) Unlock() {
b := false
bpr.Locked = &b
}
func (bpr *BuildpackRequest) Enable() {
b := true
bpr.Enabled = &b
}
func (bpr *BuildpackRequest) Disable() {
b := false
bpr.Enabled = &b
}
func (bpr *BuildpackRequest) SetPosition(i int) {
bpr.Position = &i
}
func (bpr *BuildpackRequest) SetName(s string) {
bpr.Name = &s
}
func (bpr *BuildpackRequest) SetStack(s string) {
bpr.Stack = &s
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,407 @@
package cfclient
import (
"bytes"
"crypto/tls"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
"github.com/pkg/errors"
"golang.org/x/net/context"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
//Client used to communicate with Cloud Foundry
type Client struct {
Config Config
Endpoint Endpoint
}
type Endpoint struct {
DopplerEndpoint string `json:"doppler_logging_endpoint"`
LoggingEndpoint string `json:"logging_endpoint"`
AuthEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
}
//Config is used to configure the creation of a client
type Config struct {
ApiAddress string `json:"api_url"`
Username string `json:"user"`
Password string `json:"password"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
SkipSslValidation bool `json:"skip_ssl_validation"`
HttpClient *http.Client
Token string `json:"auth_token"`
TokenSource oauth2.TokenSource
tokenSourceDeadline *time.Time
UserAgent string `json:"user_agent"`
}
// Request is used to help build up a request
type Request struct {
method string
url string
params url.Values
body io.Reader
obj interface{}
}
//DefaultConfig configuration for client
//Keep LoginAdress for backward compatibility
//Need to be remove in close future
func DefaultConfig() *Config {
return &Config{
ApiAddress: "http://api.bosh-lite.com",
Username: "admin",
Password: "admin",
Token: "",
SkipSslValidation: false,
HttpClient: http.DefaultClient,
UserAgent: "Go-CF-client/1.1",
}
}
func DefaultEndpoint() *Endpoint {
return &Endpoint{
DopplerEndpoint: "wss://doppler.10.244.0.34.xip.io:443",
LoggingEndpoint: "wss://loggregator.10.244.0.34.xip.io:443",
TokenEndpoint: "https://uaa.10.244.0.34.xip.io",
AuthEndpoint: "https://login.10.244.0.34.xip.io",
}
}
// NewClient returns a new client
func NewClient(config *Config) (client *Client, err error) {
// bootstrap the config
defConfig := DefaultConfig()
if len(config.ApiAddress) == 0 {
config.ApiAddress = defConfig.ApiAddress
}
if len(config.Username) == 0 {
config.Username = defConfig.Username
}
if len(config.Password) == 0 {
config.Password = defConfig.Password
}
if len(config.Token) == 0 {
config.Token = defConfig.Token
}
if len(config.UserAgent) == 0 {
config.UserAgent = defConfig.UserAgent
}
if config.HttpClient == nil {
config.HttpClient = defConfig.HttpClient
}
if config.HttpClient.Transport == nil {
config.HttpClient.Transport = shallowDefaultTransport()
}
var tp *http.Transport
switch t := config.HttpClient.Transport.(type) {
case *http.Transport:
tp = t
case *oauth2.Transport:
if bt, ok := t.Base.(*http.Transport); ok {
tp = bt
}
}
if tp != nil {
if tp.TLSClientConfig == nil {
tp.TLSClientConfig = &tls.Config{}
}
tp.TLSClientConfig.InsecureSkipVerify = config.SkipSslValidation
}
config.ApiAddress = strings.TrimRight(config.ApiAddress, "/")
client = &Client{
Config: *config,
}
if err := client.refreshEndpoint(); err != nil {
return nil, err
}
return client, nil
}
func shallowDefaultTransport() *http.Transport {
defaultTransport := http.DefaultTransport.(*http.Transport)
return &http.Transport{
Proxy: defaultTransport.Proxy,
TLSHandshakeTimeout: defaultTransport.TLSHandshakeTimeout,
ExpectContinueTimeout: defaultTransport.ExpectContinueTimeout,
}
}
func getUserAuth(ctx context.Context, config Config, endpoint *Endpoint) (Config, error) {
authConfig := &oauth2.Config{
ClientID: "cf",
Scopes: []string{""},
Endpoint: oauth2.Endpoint{
AuthURL: endpoint.AuthEndpoint + "/oauth/auth",
TokenURL: endpoint.TokenEndpoint + "/oauth/token",
},
}
token, err := authConfig.PasswordCredentialsToken(ctx, config.Username, config.Password)
if err != nil {
return config, errors.Wrap(err, "Error getting token")
}
config.tokenSourceDeadline = &token.Expiry
config.TokenSource = authConfig.TokenSource(ctx, token)
config.HttpClient = oauth2.NewClient(ctx, config.TokenSource)
return config, err
}
func getClientAuth(ctx context.Context, config Config, endpoint *Endpoint) Config {
authConfig := &clientcredentials.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
TokenURL: endpoint.TokenEndpoint + "/oauth/token",
}
config.TokenSource = authConfig.TokenSource(ctx)
config.HttpClient = authConfig.Client(ctx)
return config
}
// getUserTokenAuth initializes client credentials from existing bearer token.
func getUserTokenAuth(ctx context.Context, config Config, endpoint *Endpoint) Config {
authConfig := &oauth2.Config{
ClientID: "cf",
Scopes: []string{""},
Endpoint: oauth2.Endpoint{
AuthURL: endpoint.AuthEndpoint + "/oauth/auth",
TokenURL: endpoint.TokenEndpoint + "/oauth/token",
},
}
// Token is expected to have no "bearer" prefix
token := &oauth2.Token{
AccessToken: config.Token,
TokenType: "Bearer"}
config.TokenSource = authConfig.TokenSource(ctx, token)
config.HttpClient = oauth2.NewClient(ctx, config.TokenSource)
return config
}
func getInfo(api string, httpClient *http.Client) (*Endpoint, error) {
var endpoint Endpoint
if api == "" {
return DefaultEndpoint(), nil
}
resp, err := httpClient.Get(api + "/v2/info")
if err != nil {
return nil, err
}
defer resp.Body.Close()
err = decodeBody(resp, &endpoint)
if err != nil {
return nil, err
}
return &endpoint, err
}
// NewRequest is used to create a new Request
func (c *Client) NewRequest(method, path string) *Request {
r := &Request{
method: method,
url: c.Config.ApiAddress + path,
params: make(map[string][]string),
}
return r
}
// NewRequestWithBody is used to create a new request with
// arbigtrary body io.Reader.
func (c *Client) NewRequestWithBody(method, path string, body io.Reader) *Request {
r := c.NewRequest(method, path)
// Set request body
r.body = body
return r
}
// DoRequest runs a request with our client
func (c *Client) DoRequest(r *Request) (*http.Response, error) {
req, err := r.toHTTP()
if err != nil {
return nil, err
}
return c.Do(req)
}
// DoRequestWithoutRedirects executes the request without following redirects
func (c *Client) DoRequestWithoutRedirects(r *Request) (*http.Response, error) {
prevCheckRedirect := c.Config.HttpClient.CheckRedirect
c.Config.HttpClient.CheckRedirect = func(httpReq *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
defer func() {
c.Config.HttpClient.CheckRedirect = prevCheckRedirect
}()
return c.DoRequest(r)
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.Header.Set("User-Agent", c.Config.UserAgent)
if req.Body != nil && req.Header.Get("Content-type") == "" {
req.Header.Set("Content-type", "application/json")
}
resp, err := c.Config.HttpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= http.StatusBadRequest {
return c.handleError(resp)
}
return resp, nil
}
func (c *Client) handleError(resp *http.Response) (*http.Response, error) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp, CloudFoundryHTTPError{
StatusCode: resp.StatusCode,
Status: resp.Status,
Body: body,
}
}
defer resp.Body.Close()
// Unmarshal V2 error response
if strings.HasPrefix(resp.Request.URL.Path, "/v2/") {
var cfErr CloudFoundryError
if err := json.Unmarshal(body, &cfErr); err != nil {
return resp, CloudFoundryHTTPError{
StatusCode: resp.StatusCode,
Status: resp.Status,
Body: body,
}
}
return nil, cfErr
}
// Unmarshal a V3 error response and convert it into a V2 model
var cfErrorsV3 CloudFoundryErrorsV3
if err := json.Unmarshal(body, &cfErrorsV3); err != nil {
return resp, CloudFoundryHTTPError{
StatusCode: resp.StatusCode,
Status: resp.Status,
Body: body,
}
}
return nil, NewCloudFoundryErrorFromV3Errors(cfErrorsV3)
}
func (c *Client) refreshEndpoint() error {
// we want to keep the Timeout value from config.HttpClient
timeout := c.Config.HttpClient.Timeout
ctx := context.Background()
ctx = context.WithValue(ctx, oauth2.HTTPClient, c.Config.HttpClient)
endpoint, err := getInfo(c.Config.ApiAddress, oauth2.NewClient(ctx, nil))
if err != nil {
return errors.Wrap(err, "Could not get api /v2/info")
}
switch {
case c.Config.Token != "":
c.Config = getUserTokenAuth(ctx, c.Config, endpoint)
case c.Config.ClientID != "":
c.Config = getClientAuth(ctx, c.Config, endpoint)
default:
c.Config, err = getUserAuth(ctx, c.Config, endpoint)
if err != nil {
return err
}
}
// make sure original Timeout value will be used
if c.Config.HttpClient.Timeout != timeout {
c.Config.HttpClient.Timeout = timeout
}
c.Endpoint = *endpoint
return nil
}
// toHTTP converts the request to an HTTP Request
func (r *Request) toHTTP() (*http.Request, error) {
// Check if we should encode the body
if r.body == nil && r.obj != nil {
b, err := encodeBody(r.obj)
if err != nil {
return nil, err
}
r.body = b
}
// Create the HTTP Request
return http.NewRequest(r.method, r.url, r.body)
}
// decodeBody is used to JSON decode a body
func decodeBody(resp *http.Response, out interface{}) error {
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
return dec.Decode(out)
}
// encodeBody is used to encode a request body
func encodeBody(obj interface{}) (io.Reader, error) {
buf := bytes.NewBuffer(nil)
enc := json.NewEncoder(buf)
if err := enc.Encode(obj); err != nil {
return nil, err
}
return buf, nil
}
func (c *Client) GetToken() (string, error) {
if c.Config.tokenSourceDeadline != nil && c.Config.tokenSourceDeadline.Before(time.Now()) {
if err := c.refreshEndpoint(); err != nil {
return "", err
}
}
token, err := c.Config.TokenSource.Token()
if err != nil {
return "", errors.Wrap(err, "Error getting bearer token")
}
return "bearer " + token.AccessToken, nil
}

View File

@ -0,0 +1,290 @@
package cfclient
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strings"
"github.com/pkg/errors"
)
type DomainsResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []DomainResource `json:"resources"`
}
type SharedDomainsResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []SharedDomainResource `json:"resources"`
}
type DomainResource struct {
Meta Meta `json:"metadata"`
Entity Domain `json:"entity"`
}
type SharedDomainResource struct {
Meta Meta `json:"metadata"`
Entity SharedDomain `json:"entity"`
}
type Domain struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
OwningOrganizationGuid string `json:"owning_organization_guid"`
OwningOrganizationUrl string `json:"owning_organization_url"`
SharedOrganizationsUrl string `json:"shared_organizations_url"`
c *Client
}
type SharedDomain struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
RouterGroupGuid string `json:"router_group_guid"`
RouterGroupType string `json:"router_group_type"`
Internal bool `json:"internal"`
c *Client
}
func (c *Client) ListDomainsByQuery(query url.Values) ([]Domain, error) {
var domains []Domain
requestUrl := "/v2/private_domains?" + query.Encode()
for {
var domainResp DomainsResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting domains")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading domains request")
}
err = json.Unmarshal(resBody, &domainResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling domains")
}
for _, domain := range domainResp.Resources {
domain.Entity.Guid = domain.Meta.Guid
domain.Entity.CreatedAt = domain.Meta.CreatedAt
domain.Entity.UpdatedAt = domain.Meta.UpdatedAt
domain.Entity.c = c
domains = append(domains, domain.Entity)
}
requestUrl = domainResp.NextUrl
if requestUrl == "" {
break
}
}
return domains, nil
}
func (c *Client) ListDomains() ([]Domain, error) {
return c.ListDomainsByQuery(nil)
}
func (c *Client) ListSharedDomainsByQuery(query url.Values) ([]SharedDomain, error) {
var domains []SharedDomain
requestUrl := "/v2/shared_domains?" + query.Encode()
for {
var domainResp SharedDomainsResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting shared domains")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading shared domains request")
}
err = json.Unmarshal(resBody, &domainResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling shared domains")
}
for _, domain := range domainResp.Resources {
domain.Entity.Guid = domain.Meta.Guid
domain.Entity.CreatedAt = domain.Meta.CreatedAt
domain.Entity.UpdatedAt = domain.Meta.UpdatedAt
domain.Entity.c = c
domains = append(domains, domain.Entity)
}
requestUrl = domainResp.NextUrl
if requestUrl == "" {
break
}
}
return domains, nil
}
func (c *Client) ListSharedDomains() ([]SharedDomain, error) {
return c.ListSharedDomainsByQuery(nil)
}
func (c *Client) GetSharedDomainByGuid(guid string) (SharedDomain, error) {
r := c.NewRequest("GET", "/v2/shared_domains/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return SharedDomain{}, errors.Wrap(err, "Error requesting shared domain")
}
defer resp.Body.Close()
retval, err := c.handleSharedDomainResp(resp)
return *retval, err
}
func (c *Client) CreateSharedDomain(name string, internal bool, router_group_guid string) (*SharedDomain, error) {
req := c.NewRequest("POST", "/v2/shared_domains")
params := map[string]interface{}{
"name": name,
"internal": internal,
}
if strings.TrimSpace(router_group_guid) != "" {
params["router_group_guid"] = router_group_guid
}
req.obj = params
resp, err := c.DoRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, errors.Wrapf(err, "Error creating shared domain %s, response code: %d", name, resp.StatusCode)
}
return c.handleSharedDomainResp(resp)
}
func (c *Client) DeleteSharedDomain(guid string, async bool) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/shared_domains/%s?async=%t", guid, async)))
if err != nil {
return err
}
if (async && (resp.StatusCode != http.StatusAccepted)) || (!async && (resp.StatusCode != http.StatusNoContent)) {
return errors.Wrapf(err, "Error deleting organization %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) GetDomainByName(name string) (Domain, error) {
q := url.Values{}
q.Set("q", "name:"+name)
domains, err := c.ListDomainsByQuery(q)
if err != nil {
return Domain{}, errors.Wrapf(err, "Error during domain lookup %s", name)
}
if len(domains) == 0 {
return Domain{}, fmt.Errorf("Unable to find domain %s", name)
}
return domains[0], nil
}
func (c *Client) GetSharedDomainByName(name string) (SharedDomain, error) {
q := url.Values{}
q.Set("q", "name:"+name)
domains, err := c.ListSharedDomainsByQuery(q)
if err != nil {
return SharedDomain{}, errors.Wrapf(err, "Error during shared domain lookup %s", name)
}
if len(domains) == 0 {
return SharedDomain{}, fmt.Errorf("Unable to find shared domain %s", name)
}
return domains[0], nil
}
func (c *Client) CreateDomain(name, orgGuid string) (*Domain, error) {
req := c.NewRequest("POST", "/v2/private_domains")
req.obj = map[string]interface{}{
"name": name,
"owning_organization_guid": orgGuid,
}
resp, err := c.DoRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, errors.Wrapf(err, "Error creating domain %s, response code: %d", name, resp.StatusCode)
}
return c.handleDomainResp(resp)
}
func (c *Client) DeleteDomain(guid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/private_domains/%s", guid)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting domain %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) handleDomainResp(resp *http.Response) (*Domain, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
var domainResource DomainResource
err = json.Unmarshal(body, &domainResource)
if err != nil {
return nil, err
}
return c.mergeDomainResource(domainResource), nil
}
func (c *Client) handleSharedDomainResp(resp *http.Response) (*SharedDomain, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
var domainResource SharedDomainResource
err = json.Unmarshal(body, &domainResource)
if err != nil {
return nil, err
}
return c.mergeSharedDomainResource(domainResource), nil
}
func (c *Client) getDomainsResponse(requestUrl string) (DomainsResponse, error) {
var domainResp DomainsResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return DomainsResponse{}, errors.Wrap(err, "Error requesting domains")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return DomainsResponse{}, errors.Wrap(err, "Error reading domains request")
}
err = json.Unmarshal(resBody, &domainResp)
if err != nil {
return DomainsResponse{}, errors.Wrap(err, "Error unmarshalling org")
}
return domainResp, nil
}
func (c *Client) mergeDomainResource(domainResource DomainResource) *Domain {
domainResource.Entity.Guid = domainResource.Meta.Guid
domainResource.Entity.c = c
return &domainResource.Entity
}
func (c *Client) mergeSharedDomainResource(domainResource SharedDomainResource) *SharedDomain {
domainResource.Entity.Guid = domainResource.Meta.Guid
domainResource.Entity.c = c
return &domainResource.Entity
}

View File

@ -0,0 +1,59 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
)
type EnvironmentVariableGroup map[string]interface{}
func (c *Client) GetRunningEnvironmentVariableGroup() (EnvironmentVariableGroup, error) {
return c.getEnvironmentVariableGroup(true)
}
func (c *Client) GetStagingEnvironmentVariableGroup() (EnvironmentVariableGroup, error) {
return c.getEnvironmentVariableGroup(false)
}
func (c *Client) getEnvironmentVariableGroup(running bool) (EnvironmentVariableGroup, error) {
evgType := "staging"
if running {
evgType = "running"
}
req := c.NewRequest("GET", fmt.Sprintf("/v2/config/environment_variable_groups/%s", evgType))
resp, err := c.DoRequest(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
evg := EnvironmentVariableGroup{}
err = json.NewDecoder(resp.Body).Decode(&evg)
return evg, err
}
func (c *Client) SetRunningEnvironmentVariableGroup(evg EnvironmentVariableGroup) error {
return c.setEnvironmentVariableGroup(evg, true)
}
func (c *Client) SetStagingEnvironmentVariableGroup(evg EnvironmentVariableGroup) error {
return c.setEnvironmentVariableGroup(evg, false)
}
func (c *Client) setEnvironmentVariableGroup(evg EnvironmentVariableGroup, running bool) error {
evgType := "staging"
if running {
evgType = "running"
}
marshalled, err := json.Marshal(evg)
if err != nil {
return err
}
req := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/config/environment_variable_groups/%s", evgType), bytes.NewBuffer(marshalled))
_, err = c.DoRequest(req)
return err
}

View File

@ -0,0 +1,54 @@
package cfclient
//go:generate go run gen_error.go
import (
"fmt"
)
type CloudFoundryError struct {
Code int `json:"code"`
ErrorCode string `json:"error_code"`
Description string `json:"description"`
}
type CloudFoundryErrorsV3 struct {
Errors []CloudFoundryErrorV3 `json:"errors"`
}
type CloudFoundryErrorV3 struct {
Code int `json:"code"`
Title string `json:"title"`
Detail string `json:"detail"`
}
// CF APIs v3 can return multiple errors, we take the first one and convert it into a V2 model
func NewCloudFoundryErrorFromV3Errors(cfErrorsV3 CloudFoundryErrorsV3) CloudFoundryError {
if len(cfErrorsV3.Errors) == 0 {
return CloudFoundryError{
0,
"GO-Client-No-Errors",
"No Errors in response from V3",
}
}
return CloudFoundryError{
cfErrorsV3.Errors[0].Code,
cfErrorsV3.Errors[0].Title,
cfErrorsV3.Errors[0].Detail,
}
}
func (cfErr CloudFoundryError) Error() string {
return fmt.Sprintf("cfclient error (%s|%d): %s", cfErr.ErrorCode, cfErr.Code, cfErr.Description)
}
type CloudFoundryHTTPError struct {
StatusCode int
Status string
Body []byte
}
func (e CloudFoundryHTTPError) Error() string {
return fmt.Sprintf("cfclient: HTTP error (%d): %s", e.StatusCode, e.Status)
}

View File

@ -0,0 +1,94 @@
package cfclient
import (
"encoding/json"
"fmt"
"net/url"
"github.com/pkg/errors"
)
// EventsResponse is a type that wraps a collection of event resources.
type EventsResponse struct {
TotalResults int `json:"total_results"`
Pages int `json:"total_pages"`
NextURL string `json:"next_url"`
Resources []EventResource `json:"resources"`
}
// EventResource is a type that contains metadata and the entity for an event.
type EventResource struct {
Meta Meta `json:"metadata"`
Entity Event `json:"entity"`
}
// Event is a type that contains event data.
type Event struct {
GUID string `json:"guid"`
Type string `json:"type"`
CreatedAt string `json:"created_at"`
Actor string `json:"actor"`
ActorType string `json:"actor_type"`
ActorName string `json:"actor_name"`
ActorUsername string `json:"actor_username"`
Actee string `json:"actee"`
ActeeType string `json:"actee_type"`
ActeeName string `json:"actee_name"`
OrganizationGUID string `json:"organization_guid"`
SpaceGUID string `json:"space_guid"`
c *Client
}
// ListEventsByQuery lists all events matching the provided query.
func (c *Client) ListEventsByQuery(query url.Values) ([]Event, error) {
var events []Event
requestURL := fmt.Sprintf("/v2/events?%s", query.Encode())
for {
var eventResp EventsResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "error requesting events")
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&eventResp); err != nil {
return nil, errors.Wrap(err, "error unmarshaling events")
}
for _, e := range eventResp.Resources {
e.Entity.GUID = e.Meta.Guid
e.Entity.CreatedAt = e.Meta.CreatedAt
e.Entity.c = c
events = append(events, e.Entity)
}
requestURL = eventResp.NextURL
if requestURL == "" {
break
}
}
return events, nil
}
// ListEvents lists all unfiltered events.
func (c *Client) ListEvents() ([]Event, error) {
return c.ListEventsByQuery(nil)
}
// TotalEventsByQuery returns the number of events matching the provided query.
func (c *Client) TotalEventsByQuery(query url.Values) (int, error) {
r := c.NewRequest("GET", fmt.Sprintf("/v2/events?%s", query.Encode()))
resp, err := c.DoRequest(r)
if err != nil {
return 0, errors.Wrap(err, "error requesting events")
}
defer resp.Body.Close()
var apiResp EventsResponse
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
return 0, errors.Wrap(err, "error unmarshaling events")
}
return apiResp.TotalResults, nil
}
// TotalEvents returns the number of unfiltered events.
func (c *Client) TotalEvents() (int, error) {
return c.TotalEventsByQuery(nil)
}

View File

@ -0,0 +1,115 @@
// +build ignore
package main
import (
"bytes"
"go/format"
"io/ioutil"
"log"
"net/http"
"sort"
"strings"
"text/template"
"time"
"gopkg.in/yaml.v2"
)
type CFCode int
type HTTPCode int
type Definition struct {
CFCode `yaml:"-"`
Name string `yaml:"name"`
HTTPCode `yaml:"http_code"`
Message string `yaml:"message"`
}
func main() {
const url = "https://raw.githubusercontent.com/cloudfoundry/cloud_controller_ng/master/vendor/errors/v2.yml"
resp, err := http.Get(url)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Fatal(err)
}
var m map[CFCode]Definition
if err := yaml.Unmarshal(body, &m); err != nil {
log.Fatal(err)
}
var definitions []Definition
for c, d := range m {
d.CFCode = c
definitions = append(definitions, d)
}
sort.Slice(definitions, func(i, j int) bool {
return definitions[i].CFCode < definitions[j].CFCode
})
buf := &bytes.Buffer{}
if err := packageTemplate.Execute(buf, struct {
Timestamp time.Time
Definitions []Definition
}{
Timestamp: time.Now(),
Definitions: definitions,
}); err != nil {
log.Fatal(err)
}
dst, err := format.Source(buf.Bytes())
if err != nil {
log.Printf("%s", buf.Bytes())
log.Fatal(err)
}
if err := ioutil.WriteFile("cf_error.go", dst, 0600); err != nil {
log.Fatal(err)
}
}
// destutter ensures that s does not end in "Error".
func destutter(s string) string {
return strings.TrimSuffix(s, "Error")
}
var packageTemplate = template.Must(template.New("").Funcs(template.FuncMap{
"destutter": destutter,
}).Parse(`
package cfclient
// Code generated by go generate. DO NOT EDIT.
// This file was generated by robots at
// {{ .Timestamp }}
import "github.com/pkg/errors"
{{- range .Definitions }}
{{$method := printf "Is%sError" (.Name | destutter) }}
// {{ $method }} returns a boolean indicating whether
// the error is known to report the Cloud Foundry error:
// - Cloud Foundry code: {{ .CFCode }}
// - HTTP code: {{ .HTTPCode }}
// - message: {{ printf "%q" .Message }}
func Is{{ .Name | destutter }}Error(err error) bool {
cause := errors.Cause(err)
cferr, ok := cause.(CloudFoundryError)
if !ok {
return false
}
return cferr.Code == {{ .CFCode }}
}
{{- end }}
`))

View File

@ -0,0 +1,20 @@
module github.com/cloudfoundry-community/go-cfclient
require (
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f
github.com/Masterminds/semver v1.4.2
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f // indirect
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11
github.com/onsi/gomega v1.4.3
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 // indirect
github.com/pkg/errors v0.8.1
github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470 // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
)

View File

@ -0,0 +1,65 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 h1:sDMmm+q/3+BukdIpxwO365v/Rbspp2Nt5XntgQRXq8Q=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f h1:FDM3EtwZLyhW48YRiyqjivNlNZjAObv4xt4NnJaU+NQ=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11 h1:YFh+sjyJTMQSYjKwM4dFKhJPJC/wfo98tPUc17HdoYw=
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2 h1:CXwSGu/LYmbjEab5aMCs5usQRVBGThelUKBNnoSOuso=
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470 h1:R0uuDVEvfDha2O6dfJRr4/5NBHKEbZhMPZmqOWpEkSo=
github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a h1:JSvGDIbmil4Ui/dDdFBExb7/cmkNjyX5F97oglmvCDo=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3 h1:ulvT7fqt0yHWzpJwI57MezWnYDVpCAYBVuYst/L+fAY=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1 h1:VeAkjQVzKLmu+JnFcK96TPbkuaTIqwGGAzQ9hgwPjVg=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -0,0 +1,43 @@
package cfclient
import (
"encoding/json"
"github.com/pkg/errors"
)
// Info is metadata about a Cloud Foundry deployment
type Info struct {
Name string `json:"name"`
Build string `json:"build"`
Support string `json:"support"`
Version int `json:"version"`
Description string `json:"description"`
AuthorizationEndpoint string `json:"authorization_endpoint"`
TokenEndpoint string `json:"token_endpoint"`
MinCLIVersion string `json:"min_cli_version"`
MinRecommendedCLIVersion string `json:"min_recommended_cli_version"`
APIVersion string `json:"api_version"`
AppSSHEndpoint string `json:"app_ssh_endpoint"`
AppSSHHostKeyFingerprint string `json:"app_ssh_host_key_fingerprint"`
AppSSHOauthClient string `json:"app_ssh_oauth_client"`
DopplerLoggingEndpoint string `json:"doppler_logging_endpoint"`
RoutingEndpoint string `json:"routing_endpoint"`
User string `json:"user,omitempty"`
}
// GetInfo retrieves Info from the Cloud Controller API
func (c *Client) GetInfo() (*Info, error) {
r := c.NewRequest("GET", "/v2/info")
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting info")
}
defer resp.Body.Close()
var i Info
err = json.NewDecoder(resp.Body).Decode(&i)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling info")
}
return &i, nil
}

View File

@ -0,0 +1,251 @@
package cfclient
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"time"
"github.com/pkg/errors"
)
type IsolationSegment struct {
GUID string `json:"guid"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
c *Client
}
type IsolationSegementResponse struct {
GUID string `json:"guid"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Links struct {
Self struct {
Href string `json:"href"`
} `json:"self"`
Spaces struct {
Href string `json:"href"`
} `json:"spaces"`
Organizations struct {
Href string `json:"href"`
} `json:"organizations"`
} `json:"links"`
}
type ListIsolationSegmentsResponse struct {
Pagination Pagination `json:"pagination"`
Resources []IsolationSegementResponse `json:"resources"`
}
func (c *Client) CreateIsolationSegment(name string) (*IsolationSegment, error) {
req := c.NewRequest("POST", "/v3/isolation_segments")
req.obj = map[string]interface{}{
"name": name,
}
resp, err := c.DoRequest(req)
if err != nil {
return nil, errors.Wrap(err, "Error while creating isolation segment")
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("Error creating isolation segment %s, response code: %d", name, resp.StatusCode)
}
return respBodyToIsolationSegment(resp.Body, c)
}
func respBodyToIsolationSegment(body io.ReadCloser, c *Client) (*IsolationSegment, error) {
bodyRaw, err := ioutil.ReadAll(body)
if err != nil {
return nil, err
}
isr := IsolationSegementResponse{}
err = json.Unmarshal(bodyRaw, &isr)
if err != nil {
return nil, err
}
return &IsolationSegment{
GUID: isr.GUID,
Name: isr.Name,
CreatedAt: isr.CreatedAt,
UpdatedAt: isr.UpdatedAt,
c: c,
}, nil
}
func (c *Client) GetIsolationSegmentByGUID(guid string) (*IsolationSegment, error) {
var isr IsolationSegementResponse
r := c.NewRequest("GET", "/v3/isolation_segments/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting isolation segment by GUID")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading isolation segment response body")
}
err = json.Unmarshal(resBody, &isr)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling isolation segment response")
}
return &IsolationSegment{Name: isr.Name, GUID: isr.GUID, CreatedAt: isr.CreatedAt, UpdatedAt: isr.UpdatedAt, c: c}, nil
}
func (c *Client) ListIsolationSegmentsByQuery(query url.Values) ([]IsolationSegment, error) {
var iss []IsolationSegment
requestUrl := "/v3/isolation_segments?" + query.Encode()
for {
var isr ListIsolationSegmentsResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting isolation segments")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading isolation segment request")
}
err = json.Unmarshal(resBody, &isr)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling isolation segment")
}
for _, is := range isr.Resources {
iss = append(iss, IsolationSegment{
Name: is.Name,
GUID: is.GUID,
CreatedAt: is.CreatedAt,
UpdatedAt: is.UpdatedAt,
c: c,
})
}
var ok bool
requestUrl, ok = isr.Pagination.Next.(string)
if !ok || requestUrl == "" {
break
}
}
return iss, nil
}
func (c *Client) ListIsolationSegments() ([]IsolationSegment, error) {
return c.ListIsolationSegmentsByQuery(nil)
}
// TODO listOrgsForIsolationSegments
// TODO listSpacesForIsolationSegments
// TODO setDefaultIsolationSegmentForOrg
func (c *Client) DeleteIsolationSegmentByGUID(guid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v3/isolation_segments/%s", guid)))
if err != nil {
return errors.Wrap(err, "Error during sending DELETE request for isolation segments")
}
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("Error deleting isolation segment %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (i *IsolationSegment) Delete() error {
return i.c.DeleteIsolationSegmentByGUID(i.GUID)
}
func (c *Client) AddIsolationSegmentToOrg(isolationSegmentGUID, orgGUID string) error {
isoSegment := IsolationSegment{GUID: isolationSegmentGUID, c: c}
return isoSegment.AddOrg(orgGUID)
}
func (c *Client) RemoveIsolationSegmentFromOrg(isolationSegmentGUID, orgGUID string) error {
isoSegment := IsolationSegment{GUID: isolationSegmentGUID, c: c}
return isoSegment.RemoveOrg(orgGUID)
}
func (c *Client) AddIsolationSegmentToSpace(isolationSegmentGUID, spaceGUID string) error {
isoSegment := IsolationSegment{GUID: isolationSegmentGUID, c: c}
return isoSegment.AddSpace(spaceGUID)
}
func (c *Client) RemoveIsolationSegmentFromSpace(isolationSegmentGUID, spaceGUID string) error {
isoSegment := IsolationSegment{GUID: isolationSegmentGUID, c: c}
return isoSegment.RemoveSpace(spaceGUID)
}
func (i *IsolationSegment) AddOrg(orgGuid string) error {
if i == nil || i.c == nil {
return errors.New("No communication handle.")
}
req := i.c.NewRequest("POST", fmt.Sprintf("/v3/isolation_segments/%s/relationships/organizations", i.GUID))
type Entry struct {
GUID string `json:"guid"`
}
req.obj = map[string]interface{}{
"data": []Entry{{GUID: orgGuid}},
}
resp, err := i.c.DoRequest(req)
if err != nil {
return errors.Wrap(err, "Error during adding org to isolation segment")
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Error adding org %s to isolation segment %s, response code: %d", orgGuid, i.Name, resp.StatusCode)
}
return nil
}
func (i *IsolationSegment) RemoveOrg(orgGuid string) error {
if i == nil || i.c == nil {
return errors.New("No communication handle.")
}
req := i.c.NewRequest("DELETE", fmt.Sprintf("/v3/isolation_segments/%s/relationships/organizations/%s", i.GUID, orgGuid))
resp, err := i.c.DoRequest(req)
if err != nil {
return errors.Wrapf(err, "Error during removing org %s in isolation segment %s", orgGuid, i.Name)
}
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("Error deleting org %s in isolation segment %s, response code: %d", orgGuid, i.Name, resp.StatusCode)
}
return nil
}
func (i *IsolationSegment) AddSpace(spaceGuid string) error {
if i == nil || i.c == nil {
return errors.New("No communication handle.")
}
req := i.c.NewRequest("PUT", fmt.Sprintf("/v2/spaces/%s", spaceGuid))
req.obj = map[string]interface{}{
"isolation_segment_guid": i.GUID,
}
resp, err := i.c.DoRequest(req)
if err != nil {
return errors.Wrapf(err, "Error during adding space %s to isolation segment %s", spaceGuid, i.Name)
}
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("Error adding space to isolation segment %s, response code: %d", i.Name, resp.StatusCode)
}
return nil
}
func (i *IsolationSegment) RemoveSpace(spaceGuid string) error {
if i == nil || i.c == nil {
return errors.New("No communication handle.")
}
req := i.c.NewRequest("DELETE", fmt.Sprintf("/v2/spaces/%s/isolation_segment", spaceGuid))
resp, err := i.c.DoRequest(req)
if err != nil {
return errors.Wrapf(err, "Error during deleting space %s in isolation segment %s", spaceGuid, i.Name)
}
if resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("Error deleting space %s from isolation segment %s, response code: %d", spaceGuid, i.Name, resp.StatusCode)
}
return nil
}

View File

@ -0,0 +1,184 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type OrgQuotasResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []OrgQuotasResource `json:"resources"`
}
type OrgQuotasResource struct {
Meta Meta `json:"metadata"`
Entity OrgQuota `json:"entity"`
}
type OrgQuotaRequest struct {
Name string `json:"name"`
NonBasicServicesAllowed bool `json:"non_basic_services_allowed"`
TotalServices int `json:"total_services"`
TotalRoutes int `json:"total_routes"`
TotalPrivateDomains int `json:"total_private_domains"`
MemoryLimit int `json:"memory_limit"`
TrialDBAllowed bool `json:"trial_db_allowed"`
InstanceMemoryLimit int `json:"instance_memory_limit"`
AppInstanceLimit int `json:"app_instance_limit"`
AppTaskLimit int `json:"app_task_limit"`
TotalServiceKeys int `json:"total_service_keys"`
TotalReservedRoutePorts int `json:"total_reserved_route_ports"`
}
type OrgQuota struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
NonBasicServicesAllowed bool `json:"non_basic_services_allowed"`
TotalServices int `json:"total_services"`
TotalRoutes int `json:"total_routes"`
TotalPrivateDomains int `json:"total_private_domains"`
MemoryLimit int `json:"memory_limit"`
TrialDBAllowed bool `json:"trial_db_allowed"`
InstanceMemoryLimit int `json:"instance_memory_limit"`
AppInstanceLimit int `json:"app_instance_limit"`
AppTaskLimit int `json:"app_task_limit"`
TotalServiceKeys int `json:"total_service_keys"`
TotalReservedRoutePorts int `json:"total_reserved_route_ports"`
c *Client
}
func (c *Client) ListOrgQuotasByQuery(query url.Values) ([]OrgQuota, error) {
var orgQuotas []OrgQuota
requestUrl := "/v2/quota_definitions?" + query.Encode()
for {
orgQuotasResp, err := c.getOrgQuotasResponse(requestUrl)
if err != nil {
return []OrgQuota{}, err
}
for _, org := range orgQuotasResp.Resources {
org.Entity.Guid = org.Meta.Guid
org.Entity.CreatedAt = org.Meta.CreatedAt
org.Entity.UpdatedAt = org.Meta.UpdatedAt
org.Entity.c = c
orgQuotas = append(orgQuotas, org.Entity)
}
requestUrl = orgQuotasResp.NextUrl
if requestUrl == "" {
break
}
}
return orgQuotas, nil
}
func (c *Client) ListOrgQuotas() ([]OrgQuota, error) {
return c.ListOrgQuotasByQuery(nil)
}
func (c *Client) GetOrgQuotaByName(name string) (OrgQuota, error) {
q := url.Values{}
q.Set("q", "name:"+name)
orgQuotas, err := c.ListOrgQuotasByQuery(q)
if err != nil {
return OrgQuota{}, err
}
if len(orgQuotas) != 1 {
return OrgQuota{}, fmt.Errorf("Unable to find org quota " + name)
}
return orgQuotas[0], nil
}
func (c *Client) getOrgQuotasResponse(requestUrl string) (OrgQuotasResponse, error) {
var orgQuotasResp OrgQuotasResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return OrgQuotasResponse{}, errors.Wrap(err, "Error requesting org quotas")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return OrgQuotasResponse{}, errors.Wrap(err, "Error reading org quotas body")
}
err = json.Unmarshal(resBody, &orgQuotasResp)
if err != nil {
return OrgQuotasResponse{}, errors.Wrap(err, "Error unmarshalling org quotas")
}
return orgQuotasResp, nil
}
func (c *Client) CreateOrgQuota(orgQuote OrgQuotaRequest) (*OrgQuota, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(orgQuote)
if err != nil {
return nil, err
}
r := c.NewRequestWithBody("POST", "/v2/quota_definitions", buf)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleOrgQuotaResp(resp)
}
func (c *Client) UpdateOrgQuota(orgQuotaGUID string, orgQuota OrgQuotaRequest) (*OrgQuota, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(orgQuota)
if err != nil {
return nil, err
}
r := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/quota_definitions/%s", orgQuotaGUID), buf)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleOrgQuotaResp(resp)
}
func (c *Client) DeleteOrgQuota(guid string, async bool) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/quota_definitions/%s?async=%t", guid, async)))
if err != nil {
return err
}
if (async && (resp.StatusCode != http.StatusAccepted)) || (!async && (resp.StatusCode != http.StatusNoContent)) {
return errors.Wrapf(err, "Error deleting organization %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) handleOrgQuotaResp(resp *http.Response) (*OrgQuota, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
var orgQuotasResource OrgQuotasResource
err = json.Unmarshal(body, &orgQuotasResource)
if err != nil {
return nil, err
}
return c.mergeOrgQuotaResource(orgQuotasResource), nil
}
func (c *Client) mergeOrgQuotaResource(orgQuotaResource OrgQuotasResource) *OrgQuota {
orgQuotaResource.Entity.Guid = orgQuotaResource.Meta.Guid
orgQuotaResource.Entity.CreatedAt = orgQuotaResource.Meta.CreatedAt
orgQuotaResource.Entity.UpdatedAt = orgQuotaResource.Meta.UpdatedAt
orgQuotaResource.Entity.c = c
return &orgQuotaResource.Entity
}

View File

@ -0,0 +1,832 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type OrgResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []OrgResource `json:"resources"`
}
type OrgResource struct {
Meta Meta `json:"metadata"`
Entity Org `json:"entity"`
}
type OrgUserResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextURL string `json:"next_url"`
Resources []UserResource `json:"resources"`
}
type Org struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Name string `json:"name"`
Status string `json:"status"`
QuotaDefinitionGuid string `json:"quota_definition_guid"`
DefaultIsolationSegmentGuid string `json:"default_isolation_segment_guid"`
c *Client
}
type OrgSummary struct {
Guid string `json:"guid"`
Name string `json:"name"`
Status string `json:"status"`
Spaces []OrgSummarySpaces `json:"spaces"`
}
type OrgSummarySpaces struct {
Guid string `json:"guid"`
Name string `json:"name"`
ServiceCount int `json:"service_count"`
AppCount int `json:"app_count"`
MemDevTotal int `json:"mem_dev_total"`
MemProdTotal int `json:"mem_prod_total"`
}
type OrgRequest struct {
Name string `json:"name"`
Status string `json:"status,omitempty"`
QuotaDefinitionGuid string `json:"quota_definition_guid,omitempty"`
DefaultIsolationSegmentGuid string `json:"default_isolation_segment_guid,omitempty"`
}
func (c *Client) ListOrgsByQuery(query url.Values) ([]Org, error) {
var orgs []Org
requestURL := "/v2/organizations?" + query.Encode()
for {
orgResp, err := c.getOrgResponse(requestURL)
if err != nil {
return []Org{}, err
}
for _, org := range orgResp.Resources {
orgs = append(orgs, c.mergeOrgResource(org))
}
requestURL = orgResp.NextUrl
if requestURL == "" {
break
}
}
return orgs, nil
}
func (c *Client) ListOrgs() ([]Org, error) {
return c.ListOrgsByQuery(nil)
}
func (c *Client) GetOrgByName(name string) (Org, error) {
var org Org
q := url.Values{}
q.Set("q", "name:"+name)
orgs, err := c.ListOrgsByQuery(q)
if err != nil {
return org, err
}
if len(orgs) == 0 {
return org, fmt.Errorf("Unable to find org %s", name)
}
return orgs[0], nil
}
func (c *Client) GetOrgByGuid(guid string) (Org, error) {
var orgRes OrgResource
r := c.NewRequest("GET", "/v2/organizations/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return Org{}, err
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return Org{}, err
}
err = json.Unmarshal(body, &orgRes)
if err != nil {
return Org{}, err
}
return c.mergeOrgResource(orgRes), nil
}
func (c *Client) OrgSpaces(guid string) ([]Space, error) {
return c.fetchSpaces(fmt.Sprintf("/v2/organizations/%s/spaces", guid))
}
func (o *Org) Summary() (OrgSummary, error) {
var orgSummary OrgSummary
requestURL := fmt.Sprintf("/v2/organizations/%s/summary", o.Guid)
r := o.c.NewRequest("GET", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return OrgSummary{}, errors.Wrap(err, "Error requesting org summary")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return OrgSummary{}, errors.Wrap(err, "Error reading org summary body")
}
err = json.Unmarshal(resBody, &orgSummary)
if err != nil {
return OrgSummary{}, errors.Wrap(err, "Error unmarshalling org summary")
}
return orgSummary, nil
}
func (o *Org) Quota() (*OrgQuota, error) {
var orgQuota *OrgQuota
var orgQuotaResource OrgQuotasResource
if o.QuotaDefinitionGuid == "" {
return nil, nil
}
requestURL := fmt.Sprintf("/v2/quota_definitions/%s", o.QuotaDefinitionGuid)
r := o.c.NewRequest("GET", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return &OrgQuota{}, errors.Wrap(err, "Error requesting org quota")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return &OrgQuota{}, errors.Wrap(err, "Error reading org quota body")
}
err = json.Unmarshal(resBody, &orgQuotaResource)
if err != nil {
return &OrgQuota{}, errors.Wrap(err, "Error unmarshalling org quota")
}
orgQuota = &orgQuotaResource.Entity
orgQuota.Guid = orgQuotaResource.Meta.Guid
orgQuota.c = o.c
return orgQuota, nil
}
func (c *Client) ListOrgUsersByQuery(orgGUID string, query url.Values) ([]User, error) {
var users []User
requestURL := fmt.Sprintf("/v2/organizations/%s/users?%s", orgGUID, query.Encode())
for {
omResp, err := c.getOrgUserResponse(requestURL)
if err != nil {
return []User{}, err
}
for _, u := range omResp.Resources {
users = append(users, c.mergeUserResource(u))
}
requestURL = omResp.NextURL
if requestURL == "" {
break
}
}
return users, nil
}
func (c *Client) ListOrgUsers(orgGUID string) ([]User, error) {
return c.ListOrgUsersByQuery(orgGUID, nil)
}
func (c *Client) listOrgRolesByQuery(orgGUID, role string, query url.Values) ([]User, error) {
var users []User
requestURL := fmt.Sprintf("/v2/organizations/%s/%s?%s", orgGUID, role, query.Encode())
for {
omResp, err := c.getOrgUserResponse(requestURL)
if err != nil {
return []User{}, err
}
for _, u := range omResp.Resources {
users = append(users, c.mergeUserResource(u))
}
requestURL = omResp.NextURL
if requestURL == "" {
break
}
}
return users, nil
}
func (c *Client) ListOrgManagersByQuery(orgGUID string, query url.Values) ([]User, error) {
return c.listOrgRolesByQuery(orgGUID, "managers", query)
}
func (c *Client) ListOrgManagers(orgGUID string) ([]User, error) {
return c.ListOrgManagersByQuery(orgGUID, nil)
}
func (c *Client) ListOrgAuditorsByQuery(orgGUID string, query url.Values) ([]User, error) {
return c.listOrgRolesByQuery(orgGUID, "auditors", query)
}
func (c *Client) ListOrgAuditors(orgGUID string) ([]User, error) {
return c.ListOrgAuditorsByQuery(orgGUID, nil)
}
func (c *Client) ListOrgBillingManagersByQuery(orgGUID string, query url.Values) ([]User, error) {
return c.listOrgRolesByQuery(orgGUID, "billing_managers", query)
}
func (c *Client) ListOrgBillingManagers(orgGUID string) ([]User, error) {
return c.ListOrgBillingManagersByQuery(orgGUID, nil)
}
func (c *Client) AssociateOrgManager(orgGUID, userGUID string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateManager(userGUID)
}
func (c *Client) AssociateOrgManagerByUsername(orgGUID, name string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateManagerByUsername(name)
}
func (c *Client) AssociateOrgManagerByUsernameAndOrigin(orgGUID, name, origin string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateManagerByUsernameAndOrigin(name, origin)
}
func (c *Client) AssociateOrgUser(orgGUID, userGUID string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateUser(userGUID)
}
func (c *Client) AssociateOrgAuditor(orgGUID, userGUID string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateAuditor(userGUID)
}
func (c *Client) AssociateOrgUserByUsername(orgGUID, name string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateUserByUsername(name)
}
func (c *Client) AssociateOrgUserByUsernameAndOrigin(orgGUID, name, origin string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateUserByUsernameAndOrigin(name, origin)
}
func (c *Client) AssociateOrgAuditorByUsername(orgGUID, name string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateAuditorByUsername(name)
}
func (c *Client) AssociateOrgAuditorByUsernameAndOrigin(orgGUID, name, origin string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateAuditorByUsernameAndOrigin(name, origin)
}
func (c *Client) AssociateOrgBillingManager(orgGUID, userGUID string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateBillingManager(userGUID)
}
func (c *Client) AssociateOrgBillingManagerByUsername(orgGUID, name string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateBillingManagerByUsername(name)
}
func (c *Client) AssociateOrgBillingManagerByUsernameAndOrigin(orgGUID, name, origin string) (Org, error) {
org := Org{Guid: orgGUID, c: c}
return org.AssociateBillingManagerByUsernameAndOrigin(name, origin)
}
func (c *Client) RemoveOrgManager(orgGUID, userGUID string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveManager(userGUID)
}
func (c *Client) RemoveOrgManagerByUsername(orgGUID, name string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveManagerByUsername(name)
}
func (c *Client) RemoveOrgManagerByUsernameAndOrigin(orgGUID, name, origin string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveManagerByUsernameAndOrigin(name, origin)
}
func (c *Client) RemoveOrgUser(orgGUID, userGUID string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveUser(userGUID)
}
func (c *Client) RemoveOrgAuditor(orgGUID, userGUID string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveAuditor(userGUID)
}
func (c *Client) RemoveOrgUserByUsername(orgGUID, name string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveUserByUsername(name)
}
func (c *Client) RemoveOrgUserByUsernameAndOrigin(orgGUID, name, origin string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveUserByUsernameAndOrigin(name, origin)
}
func (c *Client) RemoveOrgAuditorByUsername(orgGUID, name string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveAuditorByUsername(name)
}
func (c *Client) RemoveOrgAuditorByUsernameAndOrigin(orgGUID, name, origin string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveAuditorByUsernameAndOrigin(name, origin)
}
func (c *Client) RemoveOrgBillingManager(orgGUID, userGUID string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveBillingManager(userGUID)
}
func (c *Client) RemoveOrgBillingManagerByUsername(orgGUID, name string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveBillingManagerByUsername(name)
}
func (c *Client) RemoveOrgBillingManagerByUsernameAndOrigin(orgGUID, name, origin string) error {
org := Org{Guid: orgGUID, c: c}
return org.RemoveBillingManagerByUsernameAndOrigin(name, origin)
}
func (c *Client) ListOrgSpaceQuotas(orgGUID string) ([]SpaceQuota, error) {
org := Org{Guid: orgGUID, c: c}
return org.ListSpaceQuotas()
}
func (c *Client) ListOrgPrivateDomains(orgGUID string) ([]Domain, error) {
org := Org{Guid: orgGUID, c: c}
return org.ListPrivateDomains()
}
func (c *Client) ShareOrgPrivateDomain(orgGUID, privateDomainGUID string) (*Domain, error) {
org := Org{Guid: orgGUID, c: c}
return org.SharePrivateDomain(privateDomainGUID)
}
func (c *Client) UnshareOrgPrivateDomain(orgGUID, privateDomainGUID string) error {
org := Org{Guid: orgGUID, c: c}
return org.UnsharePrivateDomain(privateDomainGUID)
}
func (o *Org) ListSpaceQuotas() ([]SpaceQuota, error) {
var spaceQuotas []SpaceQuota
requestURL := fmt.Sprintf("/v2/organizations/%s/space_quota_definitions", o.Guid)
for {
spaceQuotasResp, err := o.c.getSpaceQuotasResponse(requestURL)
if err != nil {
return []SpaceQuota{}, err
}
for _, resource := range spaceQuotasResp.Resources {
spaceQuotas = append(spaceQuotas, *o.c.mergeSpaceQuotaResource(resource))
}
requestURL = spaceQuotasResp.NextUrl
if requestURL == "" {
break
}
}
return spaceQuotas, nil
}
func (o *Org) ListPrivateDomains() ([]Domain, error) {
var domains []Domain
requestURL := fmt.Sprintf("/v2/organizations/%s/private_domains", o.Guid)
for {
domainsResp, err := o.c.getDomainsResponse(requestURL)
if err != nil {
return []Domain{}, err
}
for _, resource := range domainsResp.Resources {
domains = append(domains, *o.c.mergeDomainResource(resource))
}
requestURL = domainsResp.NextUrl
if requestURL == "" {
break
}
}
return domains, nil
}
func (o *Org) SharePrivateDomain(privateDomainGUID string) (*Domain, error) {
requestURL := fmt.Sprintf("/v2/organizations/%s/private_domains/%s", o.Guid, privateDomainGUID)
r := o.c.NewRequest("PUT", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, errors.Wrapf(err, "Error sharing domain %s for org %s, response code: %d", privateDomainGUID, o.Guid, resp.StatusCode)
}
return o.c.handleDomainResp(resp)
}
func (o *Org) UnsharePrivateDomain(privateDomainGUID string) error {
requestURL := fmt.Sprintf("/v2/organizations/%s/private_domains/%s", o.Guid, privateDomainGUID)
r := o.c.NewRequest("DELETE", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error unsharing domain %s for org %s, response code: %d", privateDomainGUID, o.Guid, resp.StatusCode)
}
return nil
}
func (o *Org) associateRole(userGUID, role string) (Org, error) {
requestURL := fmt.Sprintf("/v2/organizations/%s/%s/%s", o.Guid, role, userGUID)
r := o.c.NewRequest("PUT", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return Org{}, err
}
if resp.StatusCode != http.StatusCreated {
return Org{}, errors.Wrapf(err, "Error associating %s %s, response code: %d", role, userGUID, resp.StatusCode)
}
return o.c.handleOrgResp(resp)
}
func (o *Org) associateRoleByUsernameAndOrigin(name, role, origin string) (Org, error) {
requestURL := fmt.Sprintf("/v2/organizations/%s/%s", o.Guid, role)
buf := bytes.NewBuffer(nil)
payload := make(map[string]string)
payload["username"] = name
if origin != "" {
payload["origin"] = origin
}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return Org{}, err
}
r := o.c.NewRequestWithBody("PUT", requestURL, buf)
resp, err := o.c.DoRequest(r)
if err != nil {
return Org{}, err
}
if resp.StatusCode != http.StatusCreated {
return Org{}, errors.Wrapf(err, "Error associating %s %s, response code: %d", role, name, resp.StatusCode)
}
return o.c.handleOrgResp(resp)
}
func (o *Org) AssociateManager(userGUID string) (Org, error) {
return o.associateRole(userGUID, "managers")
}
func (o *Org) AssociateManagerByUsername(name string) (Org, error) {
return o.associateRoleByUsernameAndOrigin(name, "managers", "")
}
func (o *Org) AssociateManagerByUsernameAndOrigin(name, origin string) (Org, error) {
return o.associateRoleByUsernameAndOrigin(name, "managers", origin)
}
func (o *Org) AssociateUser(userGUID string) (Org, error) {
requestURL := fmt.Sprintf("/v2/organizations/%s/users/%s", o.Guid, userGUID)
r := o.c.NewRequest("PUT", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return Org{}, err
}
if resp.StatusCode != http.StatusCreated {
return Org{}, errors.Wrapf(err, "Error associating user %s, response code: %d", userGUID, resp.StatusCode)
}
return o.c.handleOrgResp(resp)
}
func (o *Org) AssociateAuditor(userGUID string) (Org, error) {
return o.associateRole(userGUID, "auditors")
}
func (o *Org) AssociateAuditorByUsername(name string) (Org, error) {
return o.associateRoleByUsernameAndOrigin(name, "auditors", "")
}
func (o *Org) AssociateAuditorByUsernameAndOrigin(name, origin string) (Org, error) {
return o.associateRoleByUsernameAndOrigin(name, "auditors", origin)
}
func (o *Org) AssociateBillingManager(userGUID string) (Org, error) {
return o.associateRole(userGUID, "billing_managers")
}
func (o *Org) AssociateBillingManagerByUsername(name string) (Org, error) {
return o.associateRoleByUsernameAndOrigin(name, "billing_managers", "")
}
func (o *Org) AssociateBillingManagerByUsernameAndOrigin(name, origin string) (Org, error) {
return o.associateRoleByUsernameAndOrigin(name, "billing_managers", origin)
}
func (o *Org) AssociateUserByUsername(name string) (Org, error) {
return o.associateUserByUsernameAndOrigin(name, "")
}
func (o *Org) AssociateUserByUsernameAndOrigin(name, origin string) (Org, error) {
return o.associateUserByUsernameAndOrigin(name, origin)
}
func (o *Org) associateUserByUsernameAndOrigin(name, origin string) (Org, error) {
requestURL := fmt.Sprintf("/v2/organizations/%s/users", o.Guid)
buf := bytes.NewBuffer(nil)
payload := make(map[string]string)
payload["username"] = name
if origin != "" {
payload["origin"] = origin
}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return Org{}, err
}
r := o.c.NewRequestWithBody("PUT", requestURL, buf)
resp, err := o.c.DoRequest(r)
if err != nil {
return Org{}, err
}
if resp.StatusCode != http.StatusCreated {
return Org{}, errors.Wrapf(err, "Error associating user %s, response code: %d", name, resp.StatusCode)
}
return o.c.handleOrgResp(resp)
}
func (o *Org) removeRole(userGUID, role string) error {
requestURL := fmt.Sprintf("/v2/organizations/%s/%s/%s", o.Guid, role, userGUID)
r := o.c.NewRequest("DELETE", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error removing %s %s, response code: %d", role, userGUID, resp.StatusCode)
}
return nil
}
func (o *Org) removeRoleByUsernameAndOrigin(name, role, origin string) error {
var requestURL string
var method string
buf := bytes.NewBuffer(nil)
payload := make(map[string]string)
payload["username"] = name
if origin != "" {
requestURL = fmt.Sprintf("/v2/organizations/%s/%s/remove", o.Guid, role)
method = "POST"
payload["origin"] = origin
} else {
requestURL = fmt.Sprintf("/v2/organizations/%s/%s", o.Guid, role)
method = "DELETE"
}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return err
}
r := o.c.NewRequestWithBody(method, requestURL, buf)
resp, err := o.c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error removing manager %s, response code: %d", name, resp.StatusCode)
}
return nil
}
func (o *Org) RemoveManager(userGUID string) error {
return o.removeRole(userGUID, "managers")
}
func (o *Org) RemoveManagerByUsername(name string) error {
return o.removeRoleByUsernameAndOrigin(name, "managers", "")
}
func (o *Org) RemoveManagerByUsernameAndOrigin(name, origin string) error {
return o.removeRoleByUsernameAndOrigin(name, "managers", origin)
}
func (o *Org) RemoveAuditor(userGUID string) error {
return o.removeRole(userGUID, "auditors")
}
func (o *Org) RemoveAuditorByUsername(name string) error {
return o.removeRoleByUsernameAndOrigin(name, "auditors", "")
}
func (o *Org) RemoveAuditorByUsernameAndOrigin(name, origin string) error {
return o.removeRoleByUsernameAndOrigin(name, "auditors", origin)
}
func (o *Org) RemoveBillingManager(userGUID string) error {
return o.removeRole(userGUID, "billing_managers")
}
func (o *Org) RemoveBillingManagerByUsername(name string) error {
return o.removeRoleByUsernameAndOrigin(name, "billing_managers", "")
}
func (o *Org) RemoveBillingManagerByUsernameAndOrigin(name, origin string) error {
return o.removeRoleByUsernameAndOrigin(name, "billing_managers", origin)
}
func (o *Org) RemoveUser(userGUID string) error {
requestURL := fmt.Sprintf("/v2/organizations/%s/users/%s", o.Guid, userGUID)
r := o.c.NewRequest("DELETE", requestURL)
resp, err := o.c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error removing user %s, response code: %d", userGUID, resp.StatusCode)
}
return nil
}
func (o *Org) RemoveUserByUsername(name string) error {
return o.removeUserByUsernameAndOrigin(name, "")
}
func (o *Org) RemoveUserByUsernameAndOrigin(name, origin string) error {
return o.removeUserByUsernameAndOrigin(name, origin)
}
func (o *Org) removeUserByUsernameAndOrigin(name, origin string) error {
var requestURL string
var method string
buf := bytes.NewBuffer(nil)
payload := make(map[string]string)
payload["username"] = name
if origin != "" {
payload["origin"] = origin
requestURL = fmt.Sprintf("/v2/organizations/%s/users/remove", o.Guid)
method = "POST"
} else {
requestURL = fmt.Sprintf("/v2/organizations/%s/users", o.Guid)
method = "DELETE"
}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return err
}
r := o.c.NewRequestWithBody(method, requestURL, buf)
resp, err := o.c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error removing user %s, response code: %d", name, resp.StatusCode)
}
return nil
}
func (c *Client) CreateOrg(req OrgRequest) (Org, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return Org{}, err
}
r := c.NewRequestWithBody("POST", "/v2/organizations", buf)
resp, err := c.DoRequest(r)
if err != nil {
return Org{}, err
}
if resp.StatusCode != http.StatusCreated {
return Org{}, errors.Wrapf(err, "Error creating organization, response code: %d", resp.StatusCode)
}
return c.handleOrgResp(resp)
}
func (c *Client) UpdateOrg(orgGUID string, orgRequest OrgRequest) (Org, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(orgRequest)
if err != nil {
return Org{}, err
}
r := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/organizations/%s", orgGUID), buf)
resp, err := c.DoRequest(r)
if err != nil {
return Org{}, err
}
if resp.StatusCode != http.StatusCreated {
return Org{}, errors.Wrapf(err, "Error updating organization, response code: %d", resp.StatusCode)
}
return c.handleOrgResp(resp)
}
func (c *Client) DeleteOrg(guid string, recursive, async bool) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/organizations/%s?recursive=%t&async=%t", guid, recursive, async)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting organization %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) getOrgResponse(requestURL string) (OrgResponse, error) {
var orgResp OrgResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return OrgResponse{}, errors.Wrap(err, "Error requesting orgs")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return OrgResponse{}, errors.Wrap(err, "Error reading org request")
}
err = json.Unmarshal(resBody, &orgResp)
if err != nil {
return OrgResponse{}, errors.Wrap(err, "Error unmarshalling org")
}
return orgResp, nil
}
func (c *Client) fetchOrgs(requestURL string) ([]Org, error) {
var orgs []Org
for {
orgResp, err := c.getOrgResponse(requestURL)
if err != nil {
return []Org{}, err
}
for _, org := range orgResp.Resources {
orgs = append(orgs, c.mergeOrgResource(org))
}
requestURL = orgResp.NextUrl
if requestURL == "" {
break
}
}
return orgs, nil
}
func (c *Client) handleOrgResp(resp *http.Response) (Org, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return Org{}, err
}
var orgResource OrgResource
err = json.Unmarshal(body, &orgResource)
if err != nil {
return Org{}, err
}
return c.mergeOrgResource(orgResource), nil
}
func (c *Client) getOrgUserResponse(requestURL string) (OrgUserResponse, error) {
var omResp OrgUserResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return OrgUserResponse{}, errors.Wrap(err, "error requesting org managers")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return OrgUserResponse{}, errors.Wrap(err, "error reading org managers response body")
}
if err := json.Unmarshal(resBody, &omResp); err != nil {
return OrgUserResponse{}, errors.Wrap(err, "error unmarshaling org managers")
}
return omResp, nil
}
func (c *Client) mergeOrgResource(org OrgResource) Org {
org.Entity.Guid = org.Meta.Guid
org.Entity.CreatedAt = org.Meta.CreatedAt
org.Entity.UpdatedAt = org.Meta.UpdatedAt
org.Entity.c = c
return org.Entity
}
func (c *Client) DefaultIsolationSegmentForOrg(orgGUID, isolationSegmentGUID string) error {
return c.updateOrgDefaultIsolationSegment(orgGUID, map[string]interface{}{"guid": isolationSegmentGUID})
}
func (c *Client) ResetDefaultIsolationSegmentForOrg(orgGUID string) error {
return c.updateOrgDefaultIsolationSegment(orgGUID, nil)
}
func (c *Client) updateOrgDefaultIsolationSegment(orgGUID string, data interface{}) error {
requestURL := fmt.Sprintf("/v3/organizations/%s/relationships/default_isolation_segment", orgGUID)
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(map[string]interface{}{"data": data})
if err != nil {
return err
}
r := c.NewRequestWithBody("PATCH", requestURL, buf)
resp, err := c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.Wrapf(err, "Error setting default isolation segment for org %s, response code: %d", orgGUID, resp.StatusCode)
}
return nil
}

View File

@ -0,0 +1,124 @@
package cfclient
import (
"encoding/json"
"fmt"
"net/url"
"reflect"
)
// ProcessListResponse is the json body returned from the API
type ProcessListResponse struct {
Pagination Pagination `json:"pagination"`
Processes []Process `json:"resources"`
}
// Process represents a running process in a container.
type Process struct {
GUID string `json:"guid"`
Type string `json:"type"`
Instances int `json:"instances"`
MemoryInMB int `json:"memory_in_mb"`
DiskInMB int `json:"disk_in_mb"`
Ports []int `json:"ports,omitempty"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
HealthCheck struct {
Type string `json:"type"`
Data struct {
Timeout int `json:"timeout"`
InvocationTimeout int `json:"invocation_timeout"`
Endpoint string `json:"endpoint"`
} `json:"data"`
} `json:"health_check"`
Links struct {
Self Link `json:"self"`
Scale Link `json:"scale"`
App Link `json:"app"`
Space Link `json:"space"`
Stats Link `json:"stats"`
} `json:"links"`
}
// ListAllProcesses will call the v3 processes api
func (c *Client) ListAllProcesses() ([]Process, error) {
return c.ListAllProcessesByQuery(url.Values{})
}
// ListAllProcessesByQuery will call the v3 processes api
func (c *Client) ListAllProcessesByQuery(query url.Values) ([]Process, error) {
var allProcesses []Process
urlPath := "/v3/processes"
for {
resp, err := c.getProcessPage(urlPath, query)
if err != nil {
return nil, err
}
if resp.Pagination.TotalResults == 0 {
return nil, nil
}
if allProcesses == nil {
allProcesses = make([]Process, 0, resp.Pagination.TotalResults)
}
allProcesses = append(allProcesses, resp.Processes...)
if resp.Pagination.Next == nil {
return allProcesses, nil
}
var nextURL string
if resp.Pagination.Next == nil {
return allProcesses, nil
}
switch resp.Pagination.Next.(type) {
case string:
nextURL = resp.Pagination.Next.(string)
case map[string]interface{}:
m := resp.Pagination.Next.(map[string]interface{})
u, ok := m["href"]
if ok {
nextURL = u.(string)
}
default:
return nil, fmt.Errorf("Unexpected type [%s] for next url", reflect.TypeOf(resp.Pagination.Next).String())
}
if nextURL == "" {
return allProcesses, nil
}
u, err := url.Parse(nextURL)
if err != nil {
return nil, err
}
urlPath = u.Path
query, err = url.ParseQuery(u.RawQuery)
if err != nil {
return nil, err
}
}
}
func (c *Client) getProcessPage(urlPath string, query url.Values) (*ProcessListResponse, error) {
req := c.NewRequest("GET", fmt.Sprintf("%s?%s", urlPath, query.Encode()))
resp, err := c.DoRequest(req)
if err != nil {
return nil, err
}
procResp := new(ProcessListResponse)
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(procResp)
if err != nil {
return nil, err
}
return procResp, nil
}

View File

@ -0,0 +1,159 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type RouteMappingRequest struct {
AppGUID string `json:"app_guid"`
RouteGUID string `json:"route_guid"`
AppPort int `json:"app_port"`
}
type RouteMappingResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []RouteMappingResource `json:"resources"`
}
type RouteMapping struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
AppPort int `json:"app_port"`
AppGUID string `json:"app_guid"`
RouteGUID string `json:"route_guid"`
AppUrl string `json:"app_url"`
RouteUrl string `json:"route_url"`
c *Client
}
type RouteMappingResource struct {
Meta Meta `json:"metadata"`
Entity RouteMapping `json:"entity"`
}
func (c *Client) MappingAppAndRoute(req RouteMappingRequest) (*RouteMapping, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return nil, err
}
r := c.NewRequestWithBody("POST", "/v2/route_mappings", buf)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleMappingResp(resp)
}
func (c *Client) ListRouteMappings() ([]*RouteMapping, error) {
return c.ListRouteMappingsByQuery(nil)
}
func (c *Client) ListRouteMappingsByQuery(query url.Values) ([]*RouteMapping, error) {
var routeMappings []*RouteMapping
var routeMappingsResp RouteMappingResponse
pages := 0
requestUrl := "/v2/route_mappings?" + query.Encode()
for {
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting route mappings")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading route mappings request:")
}
err = json.Unmarshal(resBody, &routeMappingsResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling route mappings")
}
for _, routeMapping := range routeMappingsResp.Resources {
routeMappings = append(routeMappings, c.mergeRouteMappingResource(routeMapping))
}
requestUrl = routeMappingsResp.NextUrl
if requestUrl == "" {
break
}
pages++
totalPages := routeMappingsResp.Pages
if totalPages > 0 && pages >= totalPages {
break
}
}
return routeMappings, nil
}
func (c *Client) GetRouteMappingByGuid(guid string) (*RouteMapping, error) {
var routeMapping RouteMappingResource
requestUrl := fmt.Sprintf("/v2/route_mappings/%s", guid)
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting route mapping")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading route mapping response body")
}
err = json.Unmarshal(resBody, &routeMapping)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshalling route mapping")
}
routeMapping.Entity.Guid = routeMapping.Meta.Guid
routeMapping.Entity.CreatedAt = routeMapping.Meta.CreatedAt
routeMapping.Entity.UpdatedAt = routeMapping.Meta.UpdatedAt
routeMapping.Entity.c = c
return &routeMapping.Entity, nil
}
func (c *Client) DeleteRouteMapping(guid string) error {
requestUrl := fmt.Sprintf("/v2/route_mappings/%s?", guid)
resp, err := c.DoRequest(c.NewRequest("DELETE", requestUrl))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting route mapping %s, response code %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) handleMappingResp(resp *http.Response) (*RouteMapping, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
var mappingResource RouteMappingResource
err = json.Unmarshal(body, &mappingResource)
if err != nil {
return nil, err
}
return c.mergeRouteMappingResource(mappingResource), nil
}
func (c *Client) mergeRouteMappingResource(mapping RouteMappingResource) *RouteMapping {
mapping.Entity.Guid = mapping.Meta.Guid
mapping.Entity.CreatedAt = mapping.Meta.CreatedAt
mapping.Entity.UpdatedAt = mapping.Meta.UpdatedAt
mapping.Entity.c = c
return &mapping.Entity
}

View File

@ -0,0 +1,168 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type RoutesResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []RoutesResource `json:"resources"`
}
type RoutesResource struct {
Meta Meta `json:"metadata"`
Entity Route `json:"entity"`
}
type RouteRequest struct {
DomainGuid string `json:"domain_guid"`
SpaceGuid string `json:"space_guid"`
Host string `json:"host"` // required for http routes
Path string `json:"path"`
Port int `json:"port"`
}
type Route struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Host string `json:"host"`
Path string `json:"path"`
DomainGuid string `json:"domain_guid"`
SpaceGuid string `json:"space_guid"`
ServiceInstanceGuid string `json:"service_instance_guid"`
Port int `json:"port"`
c *Client
}
// CreateRoute creates a regular http route
func (c *Client) CreateRoute(routeRequest RouteRequest) (Route, error) {
routesResource, err := c.createRoute("/v2/routes", routeRequest)
if nil != err {
return Route{}, err
}
return c.mergeRouteResource(routesResource), nil
}
// CreateTcpRoute creates a TCP route
func (c *Client) CreateTcpRoute(routeRequest RouteRequest) (Route, error) {
routesResource, err := c.createRoute("/v2/routes?generate_port=true", routeRequest)
if nil != err {
return Route{}, err
}
return c.mergeRouteResource(routesResource), nil
}
// BindRoute associates the specified route with the application
func (c *Client) BindRoute(routeGUID, appGUID string) error {
resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/routes/%s/apps/%s", routeGUID, appGUID)))
if err != nil {
return errors.Wrapf(err, "Error binding route %s to app %s", routeGUID, appGUID)
}
if resp.StatusCode != http.StatusCreated {
return fmt.Errorf("Error binding route %s to app %s, response code: %d", routeGUID, appGUID, resp.StatusCode)
}
return nil
}
func (c *Client) ListRoutesByQuery(query url.Values) ([]Route, error) {
return c.fetchRoutes("/v2/routes?" + query.Encode())
}
func (c *Client) fetchRoutes(requestUrl string) ([]Route, error) {
var routes []Route
for {
routesResp, err := c.getRoutesResponse(requestUrl)
if err != nil {
return []Route{}, err
}
for _, route := range routesResp.Resources {
route.Entity.Guid = route.Meta.Guid
route.Entity.CreatedAt = route.Meta.CreatedAt
route.Entity.UpdatedAt = route.Meta.UpdatedAt
route.Entity.c = c
routes = append(routes, route.Entity)
}
requestUrl = routesResp.NextUrl
if requestUrl == "" {
break
}
}
return routes, nil
}
func (c *Client) ListRoutes() ([]Route, error) {
return c.ListRoutesByQuery(nil)
}
func (c *Client) getRoutesResponse(requestUrl string) (RoutesResponse, error) {
var routesResp RoutesResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return RoutesResponse{}, errors.Wrap(err, "Error requesting routes")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return RoutesResponse{}, errors.Wrap(err, "Error reading routes body")
}
err = json.Unmarshal(resBody, &routesResp)
if err != nil {
return RoutesResponse{}, errors.Wrap(err, "Error unmarshalling routes")
}
return routesResp, nil
}
func (c *Client) createRoute(requestUrl string, routeRequest RouteRequest) (RoutesResource, error) {
var routeResp RoutesResource
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(routeRequest)
if err != nil {
return RoutesResource{}, errors.Wrap(err, "Error creating route - failed to serialize request body")
}
r := c.NewRequestWithBody("POST", requestUrl, buf)
resp, err := c.DoRequest(r)
if err != nil {
return RoutesResource{}, errors.Wrap(err, "Error creating route")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return RoutesResource{}, errors.Wrap(err, "Error creating route")
}
err = json.Unmarshal(resBody, &routeResp)
if err != nil {
return RoutesResource{}, errors.Wrap(err, "Error unmarshalling routes")
}
routeResp.Entity.c = c
return routeResp, nil
}
func (c *Client) DeleteRoute(guid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/routes/%s", guid)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting route %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) mergeRouteResource(rr RoutesResource) Route {
rr.Entity.Guid = rr.Meta.Guid
rr.Entity.CreatedAt = rr.Meta.CreatedAt
rr.Entity.UpdatedAt = rr.Meta.UpdatedAt
rr.Entity.c = c
return rr.Entity
}

View File

@ -0,0 +1,565 @@
package cfclient
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"reflect"
"strings"
"github.com/Masterminds/semver"
"github.com/pkg/errors"
)
type SecGroupResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []SecGroupResource `json:"resources"`
}
type SecGroupCreateResponse struct {
Code int `json:"code"`
ErrorCode string `json:"error_code"`
Description string `json:"description"`
}
type SecGroupResource struct {
Meta Meta `json:"metadata"`
Entity SecGroup `json:"entity"`
}
type SecGroup struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Rules []SecGroupRule `json:"rules"`
Running bool `json:"running_default"`
Staging bool `json:"staging_default"`
SpacesURL string `json:"spaces_url"`
StagingSpacesURL string `json:"staging_spaces_url"`
SpacesData []SpaceResource `json:"spaces"`
StagingSpacesData []SpaceResource `json:"staging_spaces"`
c *Client
}
type SecGroupRule struct {
Protocol string `json:"protocol"`
Ports string `json:"ports,omitempty"` //e.g. "4000-5000,9142"
Destination string `json:"destination"` //CIDR Format
Description string `json:"description,omitempty"` //Optional description
Code int `json:"code"` // ICMP code
Type int `json:"type"` //ICMP type. Only valid if Protocol=="icmp"
Log bool `json:"log,omitempty"` //If true, log this rule
}
var MinStagingSpacesVersion *semver.Version = getMinStagingSpacesVersion()
func (c *Client) ListSecGroups() (secGroups []SecGroup, err error) {
requestURL := "/v2/security_groups?inline-relations-depth=1"
for requestURL != "" {
var secGroupResp SecGroupResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting sec groups")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading sec group response body")
}
err = json.Unmarshal(resBody, &secGroupResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling sec group")
}
for _, secGroup := range secGroupResp.Resources {
secGroup.Entity.Guid = secGroup.Meta.Guid
secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt
secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt
secGroup.Entity.c = c
for i, space := range secGroup.Entity.SpacesData {
space.Entity.Guid = space.Meta.Guid
secGroup.Entity.SpacesData[i] = space
}
if len(secGroup.Entity.SpacesData) == 0 {
spaces, err := secGroup.Entity.ListSpaceResources()
if err != nil {
return nil, err
}
for _, space := range spaces {
secGroup.Entity.SpacesData = append(secGroup.Entity.SpacesData, space)
}
}
if len(secGroup.Entity.StagingSpacesData) == 0 {
spaces, err := secGroup.Entity.ListStagingSpaceResources()
if err != nil {
return nil, err
}
for _, space := range spaces {
secGroup.Entity.StagingSpacesData = append(secGroup.Entity.SpacesData, space)
}
}
secGroups = append(secGroups, secGroup.Entity)
}
requestURL = secGroupResp.NextUrl
resp.Body.Close()
}
return secGroups, nil
}
func (c *Client) ListRunningSecGroups() ([]SecGroup, error) {
secGroups := make([]SecGroup, 0)
requestURL := "/v2/config/running_security_groups"
for requestURL != "" {
var secGroupResp SecGroupResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting sec groups")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading sec group response body")
}
err = json.Unmarshal(resBody, &secGroupResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling sec group")
}
for _, secGroup := range secGroupResp.Resources {
secGroup.Entity.Guid = secGroup.Meta.Guid
secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt
secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt
secGroup.Entity.c = c
secGroups = append(secGroups, secGroup.Entity)
}
requestURL = secGroupResp.NextUrl
resp.Body.Close()
}
return secGroups, nil
}
func (c *Client) ListStagingSecGroups() ([]SecGroup, error) {
secGroups := make([]SecGroup, 0)
requestURL := "/v2/config/staging_security_groups"
for requestURL != "" {
var secGroupResp SecGroupResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting sec groups")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading sec group response body")
}
err = json.Unmarshal(resBody, &secGroupResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling sec group")
}
for _, secGroup := range secGroupResp.Resources {
secGroup.Entity.Guid = secGroup.Meta.Guid
secGroup.Entity.CreatedAt = secGroup.Meta.CreatedAt
secGroup.Entity.UpdatedAt = secGroup.Meta.UpdatedAt
secGroup.Entity.c = c
secGroups = append(secGroups, secGroup.Entity)
}
requestURL = secGroupResp.NextUrl
resp.Body.Close()
}
return secGroups, nil
}
func (c *Client) GetSecGroupByName(name string) (secGroup SecGroup, err error) {
requestURL := "/v2/security_groups?q=name:" + name
var secGroupResp SecGroupResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return secGroup, errors.Wrap(err, "Error requesting sec groups")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return secGroup, errors.Wrap(err, "Error reading sec group response body")
}
err = json.Unmarshal(resBody, &secGroupResp)
if err != nil {
return secGroup, errors.Wrap(err, "Error unmarshaling sec group")
}
if len(secGroupResp.Resources) == 0 {
return secGroup, fmt.Errorf("No security group with name %v found", name)
}
secGroup = secGroupResp.Resources[0].Entity
secGroup.Guid = secGroupResp.Resources[0].Meta.Guid
secGroup.CreatedAt = secGroupResp.Resources[0].Meta.CreatedAt
secGroup.UpdatedAt = secGroupResp.Resources[0].Meta.UpdatedAt
secGroup.c = c
resp.Body.Close()
return secGroup, nil
}
func (secGroup *SecGroup) ListSpaceResources() ([]SpaceResource, error) {
var spaceResources []SpaceResource
requestURL := secGroup.SpacesURL
for requestURL != "" {
spaceResp, err := secGroup.c.getSpaceResponse(requestURL)
if err != nil {
return []SpaceResource{}, err
}
for i, spaceRes := range spaceResp.Resources {
spaceRes.Entity.Guid = spaceRes.Meta.Guid
spaceRes.Entity.CreatedAt = spaceRes.Meta.CreatedAt
spaceRes.Entity.UpdatedAt = spaceRes.Meta.UpdatedAt
spaceResp.Resources[i] = spaceRes
}
spaceResources = append(spaceResources, spaceResp.Resources...)
requestURL = spaceResp.NextUrl
}
return spaceResources, nil
}
func (secGroup *SecGroup) ListStagingSpaceResources() ([]SpaceResource, error) {
var spaceResources []SpaceResource
requestURL := secGroup.StagingSpacesURL
for requestURL != "" {
spaceResp, err := secGroup.c.getSpaceResponse(requestURL)
if err != nil {
// if this is a 404, let's make sure that it's not because we're on a legacy system
if cause := errors.Cause(err); cause != nil {
if httpErr, ok := cause.(CloudFoundryHTTPError); ok {
if httpErr.StatusCode == 404 {
info, infoErr := secGroup.c.GetInfo()
if infoErr != nil {
return nil, infoErr
}
apiVersion, versionErr := semver.NewVersion(info.APIVersion)
if versionErr != nil {
return nil, versionErr
}
if MinStagingSpacesVersion.GreaterThan(apiVersion) {
// this is probably not really an error, we're just trying to use a non-existent api
return nil, nil
}
}
}
}
return []SpaceResource{}, err
}
for i, spaceRes := range spaceResp.Resources {
spaceRes.Entity.Guid = spaceRes.Meta.Guid
spaceRes.Entity.CreatedAt = spaceRes.Meta.CreatedAt
spaceRes.Entity.UpdatedAt = spaceRes.Meta.UpdatedAt
spaceResp.Resources[i] = spaceRes
}
spaceResources = append(spaceResources, spaceResp.Resources...)
requestURL = spaceResp.NextUrl
}
return spaceResources, nil
}
/*
CreateSecGroup contacts the CF endpoint for creating a new security group.
name: the name to give to the created security group
rules: A slice of rule objects that describe the rules that this security group enforces.
This can technically be nil or an empty slice - we won't judge you
spaceGuids: The security group will be associated with the spaces specified by the contents of this slice.
If nil, the security group will not be associated with any spaces initially.
*/
func (c *Client) CreateSecGroup(name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) {
return c.secGroupCreateHelper("/v2/security_groups", "POST", name, rules, spaceGuids)
}
/*
UpdateSecGroup contacts the CF endpoint to update an existing security group.
guid: identifies the security group that you would like to update.
name: the new name to give to the security group
rules: A slice of rule objects that describe the rules that this security group enforces.
If this is left nil, the rules will not be changed.
spaceGuids: The security group will be associated with the spaces specified by the contents of this slice.
If nil, the space associations will not be changed.
*/
func (c *Client) UpdateSecGroup(guid, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) {
return c.secGroupCreateHelper("/v2/security_groups/"+guid, "PUT", name, rules, spaceGuids)
}
/*
DeleteSecGroup contacts the CF endpoint to delete an existing security group.
guid: Indentifies the security group to be deleted.
*/
func (c *Client) DeleteSecGroup(guid string) error {
//Perform the DELETE and check for errors
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s", guid)))
if err != nil {
return err
}
if resp.StatusCode != 204 { //204 No Content
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
/*
GetSecGroup contacts the CF endpoint for fetching the info for a particular security group.
guid: Identifies the security group to fetch information from
*/
func (c *Client) GetSecGroup(guid string) (*SecGroup, error) {
//Perform the GET and check for errors
resp, err := c.DoRequest(c.NewRequest("GET", "/v2/security_groups/"+guid))
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
//get the json out of the response body
return respBodyToSecGroup(resp.Body, c)
}
/*
BindSecGroup contacts the CF endpoint to associate a space with a security group
secGUID: identifies the security group to add a space to
spaceGUID: identifies the space to associate
*/
func (c *Client) BindSecGroup(secGUID, spaceGUID string) error {
//Perform the PUT and check for errors
resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID)))
if err != nil {
return err
}
if resp.StatusCode != 201 { //201 Created
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
/*
BindSpaceStagingSecGroup contacts the CF endpoint to associate a space with a security group for staging functions only
secGUID: identifies the security group to add a space to
spaceGUID: identifies the space to associate
*/
func (c *Client) BindStagingSecGroupToSpace(secGUID, spaceGUID string) error {
//Perform the PUT and check for errors
resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/security_groups/%s/staging_spaces/%s", secGUID, spaceGUID)))
if err != nil {
return err
}
if resp.StatusCode != 201 { //201 Created
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
/*
BindRunningSecGroup contacts the CF endpoint to associate a security group
secGUID: identifies the security group to add a space to
*/
func (c *Client) BindRunningSecGroup(secGUID string) error {
//Perform the PUT and check for errors
resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/running_security_groups/%s", secGUID)))
if err != nil {
return err
}
if resp.StatusCode != 200 { //200
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
/*
UnbindRunningSecGroup contacts the CF endpoint to dis-associate a security group
secGUID: identifies the security group to add a space to
*/
func (c *Client) UnbindRunningSecGroup(secGUID string) error {
//Perform the DELETE and check for errors
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/config/running_security_groups/%s", secGUID)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent { //204
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
/*
BindStagingSecGroup contacts the CF endpoint to associate a space with a security group
secGUID: identifies the security group to add a space to
*/
func (c *Client) BindStagingSecGroup(secGUID string) error {
//Perform the PUT and check for errors
resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/config/staging_security_groups/%s", secGUID)))
if err != nil {
return err
}
if resp.StatusCode != 200 { //200
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
/*
UnbindStagingSecGroup contacts the CF endpoint to dis-associate a space with a security group
secGUID: identifies the security group to add a space to
*/
func (c *Client) UnbindStagingSecGroup(secGUID string) error {
//Perform the DELETE and check for errors
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/config/staging_security_groups/%s", secGUID)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent { //204
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
/*
UnbindSecGroup contacts the CF endpoint to dissociate a space from a security group
secGUID: identifies the security group to remove a space from
spaceGUID: identifies the space to dissociate from the security group
*/
func (c *Client) UnbindSecGroup(secGUID, spaceGUID string) error {
//Perform the DELETE and check for errors
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/security_groups/%s/spaces/%s", secGUID, spaceGUID)))
if err != nil {
return err
}
if resp.StatusCode != 204 { //204 No Content
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
//Reads most security group response bodies into a SecGroup object
func respBodyToSecGroup(body io.ReadCloser, c *Client) (*SecGroup, error) {
//get the json from the response body
bodyRaw, err := ioutil.ReadAll(body)
if err != nil {
return nil, errors.Wrap(err, "Could not read response body")
}
jStruct := SecGroupResource{}
//make it a SecGroup
err = json.Unmarshal(bodyRaw, &jStruct)
if err != nil {
return nil, errors.Wrap(err, "Could not unmarshal response body as json")
}
//pull a few extra fields from other places
ret := jStruct.Entity
ret.Guid = jStruct.Meta.Guid
ret.CreatedAt = jStruct.Meta.CreatedAt
ret.UpdatedAt = jStruct.Meta.UpdatedAt
ret.c = c
return &ret, nil
}
func convertStructToMap(st interface{}) map[string]interface{} {
reqRules := make(map[string]interface{})
v := reflect.ValueOf(st)
t := reflect.TypeOf(st)
for i := 0; i < v.NumField(); i++ {
key := strings.ToLower(t.Field(i).Name)
typ := v.FieldByName(t.Field(i).Name).Kind().String()
structTag := t.Field(i).Tag.Get("json")
jsonName := strings.TrimSpace(strings.Split(structTag, ",")[0])
value := v.FieldByName(t.Field(i).Name)
// if jsonName is not empty use it for the key
if jsonName != "" {
key = jsonName
}
if typ == "string" {
if !(value.String() == "" && strings.Contains(structTag, "omitempty")) {
reqRules[key] = value.String()
}
} else if typ == "int" {
reqRules[key] = value.Int()
} else {
reqRules[key] = value.Interface()
}
}
return reqRules
}
//Create and Update secGroup pretty much do the same thing, so this function abstracts those out.
func (c *Client) secGroupCreateHelper(url, method, name string, rules []SecGroupRule, spaceGuids []string) (*SecGroup, error) {
reqRules := make([]map[string]interface{}, len(rules))
for i, rule := range rules {
reqRules[i] = convertStructToMap(rule)
protocol := strings.ToLower(reqRules[i]["protocol"].(string))
// if not icmp protocol need to remove the Code/Type fields
if protocol != "icmp" {
delete(reqRules[i], "code")
delete(reqRules[i], "type")
}
}
req := c.NewRequest(method, url)
//set up request body
inputs := map[string]interface{}{
"name": name,
"rules": reqRules,
}
if spaceGuids != nil {
inputs["space_guids"] = spaceGuids
}
req.obj = inputs
//fire off the request and check for problems
resp, err := c.DoRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 201 { // Both create and update should give 201 CREATED
var response SecGroupCreateResponse
bodyRaw, _ := ioutil.ReadAll(resp.Body)
err = json.Unmarshal(bodyRaw, &response)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling response")
}
return nil, fmt.Errorf(`Request failed CF API returned with status code %d
-------------------------------
Error Code %s
Code %d
Description %s`,
resp.StatusCode, response.ErrorCode, response.Code, response.Description)
}
//get the json from the response body
return respBodyToSecGroup(resp.Body, c)
}
func getMinStagingSpacesVersion() *semver.Version {
v, _ := semver.NewVersion("2.68.0")
return v
}

View File

@ -0,0 +1,176 @@
package cfclient
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type ServiceBindingsResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
Resources []ServiceBindingResource `json:"resources"`
NextUrl string `json:"next_url"`
}
type ServiceBindingResource struct {
Meta Meta `json:"metadata"`
Entity ServiceBinding `json:"entity"`
}
type ServiceBinding struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
AppGuid string `json:"app_guid"`
ServiceInstanceGuid string `json:"service_instance_guid"`
Credentials interface{} `json:"credentials"`
BindingOptions interface{} `json:"binding_options"`
GatewayData interface{} `json:"gateway_data"`
GatewayName string `json:"gateway_name"`
SyslogDrainUrl string `json:"syslog_drain_url"`
VolumeMounts interface{} `json:"volume_mounts"`
AppUrl string `json:"app_url"`
ServiceInstanceUrl string `json:"service_instance_url"`
c *Client
}
func (c *Client) ListServiceBindingsByQuery(query url.Values) ([]ServiceBinding, error) {
var serviceBindings []ServiceBinding
requestUrl := "/v2/service_bindings?" + query.Encode()
for {
var serviceBindingsResp ServiceBindingsResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting service bindings")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading service bindings request:")
}
err = json.Unmarshal(resBody, &serviceBindingsResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling service bindings")
}
for _, serviceBinding := range serviceBindingsResp.Resources {
serviceBinding.Entity.Guid = serviceBinding.Meta.Guid
serviceBinding.Entity.CreatedAt = serviceBinding.Meta.CreatedAt
serviceBinding.Entity.UpdatedAt = serviceBinding.Meta.UpdatedAt
serviceBinding.Entity.c = c
serviceBindings = append(serviceBindings, serviceBinding.Entity)
}
requestUrl = serviceBindingsResp.NextUrl
if requestUrl == "" {
break
}
}
return serviceBindings, nil
}
func (c *Client) ListServiceBindings() ([]ServiceBinding, error) {
return c.ListServiceBindingsByQuery(nil)
}
func (c *Client) GetServiceBindingByGuid(guid string) (ServiceBinding, error) {
var serviceBinding ServiceBindingResource
r := c.NewRequest("GET", "/v2/service_bindings/"+url.QueryEscape(guid))
resp, err := c.DoRequest(r)
if err != nil {
return ServiceBinding{}, errors.Wrap(err, "Error requesting serving binding")
}
defer resp.Body.Close()
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ServiceBinding{}, errors.Wrap(err, "Error reading service binding response body")
}
err = json.Unmarshal(resBody, &serviceBinding)
if err != nil {
return ServiceBinding{}, errors.Wrap(err, "Error unmarshalling service binding")
}
serviceBinding.Entity.Guid = serviceBinding.Meta.Guid
serviceBinding.Entity.CreatedAt = serviceBinding.Meta.CreatedAt
serviceBinding.Entity.UpdatedAt = serviceBinding.Meta.UpdatedAt
serviceBinding.Entity.c = c
return serviceBinding.Entity, nil
}
func (c *Client) ServiceBindingByGuid(guid string) (ServiceBinding, error) {
return c.GetServiceBindingByGuid(guid)
}
func (c *Client) DeleteServiceBinding(guid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/service_bindings/%s", guid)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting service binding %s, response code %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) CreateServiceBinding(appGUID, serviceInstanceGUID string) (*ServiceBinding, error) {
req := c.NewRequest("POST", fmt.Sprintf("/v2/service_bindings"))
req.obj = map[string]interface{}{
"app_guid": appGUID,
"service_instance_guid": serviceInstanceGUID,
}
resp, err := c.DoRequest(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, errors.Wrapf(err, "Error binding app %s to service instance %s, response code %d", appGUID, serviceInstanceGUID, resp.StatusCode)
}
return c.handleServiceBindingResp(resp)
}
func (c *Client) CreateRouteServiceBinding(routeGUID, serviceInstanceGUID string) error {
req := c.NewRequest("PUT", fmt.Sprintf("/v2/user_provided_service_instances/%s/routes/%s", serviceInstanceGUID, routeGUID))
resp, err := c.DoRequest(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated {
return errors.Wrapf(err, "Error binding route %s to service instance %s, response code %d", routeGUID, serviceInstanceGUID, resp.StatusCode)
}
return nil
}
func (c *Client) DeleteRouteServiceBinding(routeGUID, serviceInstanceGUID string) error {
req := c.NewRequest("DELETE", fmt.Sprintf("/v2/service_instances/%s/routes/%s", serviceInstanceGUID, routeGUID))
resp, err := c.DoRequest(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.Wrapf(err, "Error deleting bound route %s from service instance %s, response code %d", routeGUID, serviceInstanceGUID, resp.StatusCode)
}
return nil
}
func (c *Client) handleServiceBindingResp(resp *http.Response) (*ServiceBinding, error) {
defer resp.Body.Close()
var sb ServiceBindingResource
err := json.NewDecoder(resp.Body).Decode(&sb)
if err != nil {
return nil, err
}
return c.mergeServiceBindingResource(sb), nil
}
func (c *Client) mergeServiceBindingResource(serviceBinding ServiceBindingResource) *ServiceBinding {
serviceBinding.Entity.Guid = serviceBinding.Meta.Guid
serviceBinding.Entity.c = c
return &serviceBinding.Entity
}

View File

@ -0,0 +1,207 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type ServiceBrokerResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []ServiceBrokerResource `json:"resources"`
}
type ServiceBrokerResource struct {
Meta Meta `json:"metadata"`
Entity ServiceBroker `json:"entity"`
}
type UpdateServiceBrokerRequest struct {
Name string `json:"name"`
BrokerURL string `json:"broker_url"`
Username string `json:"auth_username"`
Password string `json:"auth_password"`
}
type CreateServiceBrokerRequest struct {
Name string `json:"name"`
BrokerURL string `json:"broker_url"`
Username string `json:"auth_username"`
Password string `json:"auth_password"`
SpaceGUID string `json:"space_guid,omitempty"`
}
type ServiceBroker struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
BrokerURL string `json:"broker_url"`
Username string `json:"auth_username"`
Password string `json:"auth_password"`
SpaceGUID string `json:"space_guid,omitempty"`
c *Client
}
func (c *Client) DeleteServiceBroker(guid string) error {
requestUrl := fmt.Sprintf("/v2/service_brokers/%s", guid)
r := c.NewRequest("DELETE", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleteing service broker %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) UpdateServiceBroker(guid string, usb UpdateServiceBrokerRequest) (ServiceBroker, error) {
var serviceBrokerResource ServiceBrokerResource
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(usb)
if err != nil {
return ServiceBroker{}, err
}
req := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/service_brokers/%s", guid), buf)
resp, err := c.DoRequest(req)
if err != nil {
return ServiceBroker{}, err
}
if resp.StatusCode != http.StatusOK {
return ServiceBroker{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return ServiceBroker{}, err
}
err = json.Unmarshal(body, &serviceBrokerResource)
if err != nil {
return ServiceBroker{}, err
}
serviceBrokerResource.Entity.Guid = serviceBrokerResource.Meta.Guid
return serviceBrokerResource.Entity, nil
}
func (c *Client) CreateServiceBroker(csb CreateServiceBrokerRequest) (ServiceBroker, error) {
var serviceBrokerResource ServiceBrokerResource
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(csb)
if err != nil {
return ServiceBroker{}, err
}
req := c.NewRequestWithBody("POST", "/v2/service_brokers", buf)
resp, err := c.DoRequest(req)
if err != nil {
return ServiceBroker{}, err
}
if resp.StatusCode != http.StatusCreated {
return ServiceBroker{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return ServiceBroker{}, err
}
err = json.Unmarshal(body, &serviceBrokerResource)
if err != nil {
return ServiceBroker{}, err
}
serviceBrokerResource.Entity.Guid = serviceBrokerResource.Meta.Guid
return serviceBrokerResource.Entity, nil
}
func (c *Client) ListServiceBrokersByQuery(query url.Values) ([]ServiceBroker, error) {
var sbs []ServiceBroker
requestUrl := "/v2/service_brokers?" + query.Encode()
for {
serviceBrokerResp, err := c.getServiceBrokerResponse(requestUrl)
if err != nil {
return []ServiceBroker{}, err
}
for _, sb := range serviceBrokerResp.Resources {
sb.Entity.Guid = sb.Meta.Guid
sb.Entity.CreatedAt = sb.Meta.CreatedAt
sb.Entity.UpdatedAt = sb.Meta.UpdatedAt
sbs = append(sbs, sb.Entity)
}
requestUrl = serviceBrokerResp.NextUrl
if requestUrl == "" {
break
}
}
return sbs, nil
}
func (c *Client) ListServiceBrokers() ([]ServiceBroker, error) {
return c.ListServiceBrokersByQuery(nil)
}
func (c *Client) GetServiceBrokerByGuid(guid string) (ServiceBroker, error) {
var serviceBrokerRes ServiceBrokerResource
r := c.NewRequest("GET", "/v2/service_brokers/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return ServiceBroker{}, err
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return ServiceBroker{}, err
}
err = json.Unmarshal(body, &serviceBrokerRes)
if err != nil {
return ServiceBroker{}, err
}
serviceBrokerRes.Entity.Guid = serviceBrokerRes.Meta.Guid
serviceBrokerRes.Entity.CreatedAt = serviceBrokerRes.Meta.CreatedAt
serviceBrokerRes.Entity.UpdatedAt = serviceBrokerRes.Meta.UpdatedAt
return serviceBrokerRes.Entity, nil
}
func (c *Client) GetServiceBrokerByName(name string) (ServiceBroker, error) {
var sb ServiceBroker
q := url.Values{}
q.Set("q", "name:"+name)
sbs, err := c.ListServiceBrokersByQuery(q)
if err != nil {
return sb, err
}
if len(sbs) == 0 {
return sb, fmt.Errorf("Unable to find service broker %s", name)
}
return sbs[0], nil
}
func (c *Client) getServiceBrokerResponse(requestUrl string) (ServiceBrokerResponse, error) {
var serviceBrokerResp ServiceBrokerResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return ServiceBrokerResponse{}, errors.Wrap(err, "Error requesting Service Brokers")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return ServiceBrokerResponse{}, errors.Wrap(err, "Error reading Service Broker request")
}
err = json.Unmarshal(resBody, &serviceBrokerResp)
if err != nil {
return ServiceBrokerResponse{}, errors.Wrap(err, "Error unmarshalling Service Broker")
}
return serviceBrokerResp, nil
}

View File

@ -0,0 +1,186 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type ServiceInstancesResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []ServiceInstanceResource `json:"resources"`
}
type ServiceInstanceRequest struct {
Name string `json:"name"`
SpaceGuid string `json:"space_guid"`
ServicePlanGuid string `json:"service_plan_guid"`
Parameters map[string]interface{} `json:"parameters,omitempty"`
Tags []string `json:"tags,omitempty"`
}
type ServiceInstanceResource struct {
Meta Meta `json:"metadata"`
Entity ServiceInstance `json:"entity"`
}
type ServiceInstance struct {
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Credentials map[string]interface{} `json:"credentials"`
ServicePlanGuid string `json:"service_plan_guid"`
SpaceGuid string `json:"space_guid"`
DashboardUrl string `json:"dashboard_url"`
Type string `json:"type"`
LastOperation LastOperation `json:"last_operation"`
Tags []string `json:"tags"`
ServiceGuid string `json:"service_guid"`
SpaceUrl string `json:"space_url"`
ServicePlanUrl string `json:"service_plan_url"`
ServiceBindingsUrl string `json:"service_bindings_url"`
ServiceKeysUrl string `json:"service_keys_url"`
RoutesUrl string `json:"routes_url"`
ServiceUrl string `json:"service_url"`
Guid string `json:"guid"`
c *Client
}
type LastOperation struct {
Type string `json:"type"`
State string `json:"state"`
Description string `json:"description"`
UpdatedAt string `json:"updated_at"`
CreatedAt string `json:"created_at"`
}
func (c *Client) ListServiceInstancesByQuery(query url.Values) ([]ServiceInstance, error) {
var instances []ServiceInstance
requestUrl := "/v2/service_instances?" + query.Encode()
for {
var sir ServiceInstancesResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting service instances")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading service instances request:")
}
err = json.Unmarshal(resBody, &sir)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling service instances")
}
for _, instance := range sir.Resources {
instances = append(instances, c.mergeServiceInstance(instance))
}
requestUrl = sir.NextUrl
if requestUrl == "" {
break
}
}
return instances, nil
}
func (c *Client) ListServiceInstances() ([]ServiceInstance, error) {
return c.ListServiceInstancesByQuery(nil)
}
func (c *Client) GetServiceInstanceByGuid(guid string) (ServiceInstance, error) {
var sir ServiceInstanceResource
req := c.NewRequest("GET", "/v2/service_instances/"+guid)
res, err := c.DoRequest(req)
if err != nil {
return ServiceInstance{}, errors.Wrap(err, "Error requesting service instance")
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return ServiceInstance{}, errors.Wrap(err, "Error reading service instance response")
}
err = json.Unmarshal(data, &sir)
if err != nil {
return ServiceInstance{}, errors.Wrap(err, "Error JSON parsing service instance response")
}
return c.mergeServiceInstance(sir), nil
}
func (c *Client) ServiceInstanceByGuid(guid string) (ServiceInstance, error) {
return c.GetServiceInstanceByGuid(guid)
}
func (c *Client) mergeServiceInstance(instance ServiceInstanceResource) ServiceInstance {
instance.Entity.Guid = instance.Meta.Guid
instance.Entity.CreatedAt = instance.Meta.CreatedAt
instance.Entity.UpdatedAt = instance.Meta.UpdatedAt
instance.Entity.c = c
return instance.Entity
}
func (c *Client) CreateServiceInstance(req ServiceInstanceRequest) (ServiceInstance, error) {
var sir ServiceInstanceResource
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return ServiceInstance{}, err
}
r := c.NewRequestWithBody("POST", "/v2/service_instances?accepts_incomplete=true", buf)
res, err := c.DoRequest(r)
if err != nil {
return ServiceInstance{}, err
}
if res.StatusCode != http.StatusAccepted && res.StatusCode != http.StatusCreated {
return ServiceInstance{}, errors.Wrapf(err, "Error creating service, response code: %d", res.StatusCode)
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return ServiceInstance{}, errors.Wrap(err, "Error reading service instance response")
}
err = json.Unmarshal(data, &sir)
if err != nil {
return ServiceInstance{}, errors.Wrap(err, "Error JSON parsing service instance response")
}
return c.mergeServiceInstance(sir), nil
}
func (c *Client) UpdateServiceInstance(serviceInstanceGuid string, updatedConfiguration io.Reader, async bool) error {
u := fmt.Sprintf("/v2/service_instances/%s?accepts_incomplete=%t", serviceInstanceGuid, async)
resp, err := c.DoRequest(c.NewRequestWithBody("PUT", u, updatedConfiguration))
if err != nil {
return err
}
if resp.StatusCode != http.StatusAccepted {
return errors.Wrapf(err, "Error updating service instance %s, response code %d", serviceInstanceGuid, resp.StatusCode)
}
return nil
}
func (c *Client) DeleteServiceInstance(guid string, recursive, async bool) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/service_instances/%s?recursive=%t&accepts_incomplete=%t&async=%t", guid, recursive, async, async)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusAccepted {
return errors.Wrapf(err, "Error deleting service instance %s, response code %d", guid, resp.StatusCode)
}
return nil
}

View File

@ -0,0 +1,171 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type ServiceKeysResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
Resources []ServiceKeyResource `json:"resources"`
NextUrl string `json:"next_url"`
}
type ServiceKeyResource struct {
Meta Meta `json:"metadata"`
Entity ServiceKey `json:"entity"`
}
type CreateServiceKeyRequest struct {
Name string `json:"name"`
ServiceInstanceGuid string `json:"service_instance_guid"`
Parameters interface{} `json:"parameters,omitempty"`
}
type ServiceKey struct {
Name string `json:"name"`
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ServiceInstanceGuid string `json:"service_instance_guid"`
Credentials interface{} `json:"credentials"`
ServiceInstanceUrl string `json:"service_instance_url"`
c *Client
}
func (c *Client) ListServiceKeysByQuery(query url.Values) ([]ServiceKey, error) {
var serviceKeys []ServiceKey
requestUrl := "/v2/service_keys?" + query.Encode()
for {
var serviceKeysResp ServiceKeysResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting service keys")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading service keys request:")
}
err = json.Unmarshal(resBody, &serviceKeysResp)
if err != nil {
return nil, errors.Wrapf(err, "Error unmarshaling service keys: %q", string(resBody))
}
for _, serviceKey := range serviceKeysResp.Resources {
serviceKey.Entity.Guid = serviceKey.Meta.Guid
serviceKey.Entity.CreatedAt = serviceKey.Meta.CreatedAt
serviceKey.Entity.UpdatedAt = serviceKey.Meta.UpdatedAt
serviceKey.Entity.c = c
serviceKeys = append(serviceKeys, serviceKey.Entity)
}
requestUrl = serviceKeysResp.NextUrl
if requestUrl == "" {
break
}
}
return serviceKeys, nil
}
func (c *Client) ListServiceKeys() ([]ServiceKey, error) {
return c.ListServiceKeysByQuery(nil)
}
func (c *Client) GetServiceKeyByName(name string) (ServiceKey, error) {
var serviceKey ServiceKey
q := url.Values{}
q.Set("q", "name:"+name)
serviceKeys, err := c.ListServiceKeysByQuery(q)
if err != nil {
return serviceKey, err
}
if len(serviceKeys) == 0 {
return serviceKey, fmt.Errorf("Unable to find service key %s", name)
}
return serviceKeys[0], nil
}
// GetServiceKeyByInstanceGuid is deprecated in favor of GetServiceKeysByInstanceGuid
func (c *Client) GetServiceKeyByInstanceGuid(guid string) (ServiceKey, error) {
var serviceKey ServiceKey
q := url.Values{}
q.Set("q", "service_instance_guid:"+guid)
serviceKeys, err := c.ListServiceKeysByQuery(q)
if err != nil {
return serviceKey, err
}
if len(serviceKeys) == 0 {
return serviceKey, fmt.Errorf("Unable to find service key for guid %s", guid)
}
return serviceKeys[0], nil
}
// GetServiceKeysByInstanceGuid returns the service keys for a service instance.
// If none are found, it returns an error.
func (c *Client) GetServiceKeysByInstanceGuid(guid string) ([]ServiceKey, error) {
q := url.Values{}
q.Set("q", "service_instance_guid:"+guid)
serviceKeys, err := c.ListServiceKeysByQuery(q)
if err != nil {
return serviceKeys, err
}
if len(serviceKeys) == 0 {
return serviceKeys, fmt.Errorf("Unable to find service key for guid %s", guid)
}
return serviceKeys, nil
}
// CreateServiceKey creates a service key from the request. If a service key
// exists already, it returns an error containing `CF-ServiceKeyNameTaken`
func (c *Client) CreateServiceKey(csr CreateServiceKeyRequest) (ServiceKey, error) {
var serviceKeyResource ServiceKeyResource
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(csr)
if err != nil {
return ServiceKey{}, err
}
req := c.NewRequestWithBody("POST", "/v2/service_keys", buf)
resp, err := c.DoRequest(req)
if err != nil {
return ServiceKey{}, err
}
if resp.StatusCode != http.StatusCreated {
return ServiceKey{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return ServiceKey{}, err
}
err = json.Unmarshal(body, &serviceKeyResource)
if err != nil {
return ServiceKey{}, err
}
return serviceKeyResource.Entity, nil
}
// DeleteServiceKey removes a service key instance
func (c *Client) DeleteServiceKey(guid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/service_keys/%s", guid)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting service instance key %s, response code %d", guid, resp.StatusCode)
}
return nil
}

View File

@ -0,0 +1,169 @@
package cfclient
import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type ServicePlanVisibilitiesResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []ServicePlanVisibilityResource `json:"resources"`
}
type ServicePlanVisibilityResource struct {
Meta Meta `json:"metadata"`
Entity ServicePlanVisibility `json:"entity"`
}
type ServicePlanVisibility struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
ServicePlanGuid string `json:"service_plan_guid"`
OrganizationGuid string `json:"organization_guid"`
ServicePlanUrl string `json:"service_plan_url"`
OrganizationUrl string `json:"organization_url"`
c *Client
}
func (c *Client) ListServicePlanVisibilitiesByQuery(query url.Values) ([]ServicePlanVisibility, error) {
var servicePlanVisibilities []ServicePlanVisibility
requestUrl := "/v2/service_plan_visibilities?" + query.Encode()
for {
var servicePlanVisibilitiesResp ServicePlanVisibilitiesResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting service plan visibilities")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading service plan visibilities request:")
}
err = json.Unmarshal(resBody, &servicePlanVisibilitiesResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling service plan visibilities")
}
for _, servicePlanVisibility := range servicePlanVisibilitiesResp.Resources {
servicePlanVisibility.Entity.Guid = servicePlanVisibility.Meta.Guid
servicePlanVisibility.Entity.CreatedAt = servicePlanVisibility.Meta.CreatedAt
servicePlanVisibility.Entity.UpdatedAt = servicePlanVisibility.Meta.UpdatedAt
servicePlanVisibility.Entity.c = c
servicePlanVisibilities = append(servicePlanVisibilities, servicePlanVisibility.Entity)
}
requestUrl = servicePlanVisibilitiesResp.NextUrl
if requestUrl == "" {
break
}
}
return servicePlanVisibilities, nil
}
func (c *Client) ListServicePlanVisibilities() ([]ServicePlanVisibility, error) {
return c.ListServicePlanVisibilitiesByQuery(nil)
}
func (c *Client) GetServicePlanVisibilityByGuid(guid string) (ServicePlanVisibility, error) {
r := c.NewRequest("GET", "/v2/service_plan_visibilities/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return ServicePlanVisibility{}, err
}
return respBodyToServicePlanVisibility(resp.Body, c)
}
//a uniqueID is the id of the service in the catalog and not in cf internal db
func (c *Client) CreateServicePlanVisibilityByUniqueId(uniqueId string, organizationGuid string) (ServicePlanVisibility, error) {
q := url.Values{}
q.Set("q", fmt.Sprintf("unique_id:%s", uniqueId))
plans, err := c.ListServicePlansByQuery(q)
if err != nil {
return ServicePlanVisibility{}, errors.Wrap(err, fmt.Sprintf("Couldn't find a service plan with unique_id: %s", uniqueId))
}
return c.CreateServicePlanVisibility(plans[0].Guid, organizationGuid)
}
func (c *Client) CreateServicePlanVisibility(servicePlanGuid string, organizationGuid string) (ServicePlanVisibility, error) {
req := c.NewRequest("POST", "/v2/service_plan_visibilities")
req.obj = map[string]interface{}{
"service_plan_guid": servicePlanGuid,
"organization_guid": organizationGuid,
}
resp, err := c.DoRequest(req)
if err != nil {
return ServicePlanVisibility{}, err
}
if resp.StatusCode != http.StatusCreated {
return ServicePlanVisibility{}, errors.Wrapf(err, "Error creating service plan visibility, response code: %d", resp.StatusCode)
}
return respBodyToServicePlanVisibility(resp.Body, c)
}
func (c *Client) DeleteServicePlanVisibilityByPlanAndOrg(servicePlanGuid string, organizationGuid string, async bool) error {
q := url.Values{}
q.Set("q", fmt.Sprintf("organization_guid:%s;service_plan_guid:%s", organizationGuid, servicePlanGuid))
plans, err := c.ListServicePlanVisibilitiesByQuery(q)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("Couldn't find a service plan visibility for service plan %s and org %s", servicePlanGuid, organizationGuid))
}
if len(plans) != 1 {
return fmt.Errorf("Query for a service plan visibility did not return exactly one result when searching for a service plan visibility for service plan %s and org %s",
servicePlanGuid, organizationGuid)
}
return c.DeleteServicePlanVisibility(plans[0].Guid, async)
}
func (c *Client) DeleteServicePlanVisibility(guid string, async bool) error {
req := c.NewRequest("DELETE", fmt.Sprintf("/v2/service_plan_visibilities/%s?async=%v", guid, async))
resp, err := c.DoRequest(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting service plan visibility, response code: %d", resp.StatusCode)
}
return nil
}
func (c *Client) UpdateServicePlanVisibility(guid string, servicePlanGuid string, organizationGuid string) (ServicePlanVisibility, error) {
req := c.NewRequest("PUT", "/v2/service_plan_visibilities/"+guid)
req.obj = map[string]interface{}{
"service_plan_guid": servicePlanGuid,
"organization_guid": organizationGuid,
}
resp, err := c.DoRequest(req)
if err != nil {
return ServicePlanVisibility{}, err
}
if resp.StatusCode != http.StatusCreated {
return ServicePlanVisibility{}, errors.Wrapf(err, "Error updating service plan visibility, response code: %d", resp.StatusCode)
}
return respBodyToServicePlanVisibility(resp.Body, c)
}
func respBodyToServicePlanVisibility(body io.ReadCloser, c *Client) (ServicePlanVisibility, error) {
bodyRaw, err := ioutil.ReadAll(body)
if err != nil {
return ServicePlanVisibility{}, err
}
servicePlanVisibilityRes := ServicePlanVisibilityResource{}
err = json.Unmarshal(bodyRaw, &servicePlanVisibilityRes)
if err != nil {
return ServicePlanVisibility{}, err
}
servicePlanVisibility := servicePlanVisibilityRes.Entity
servicePlanVisibility.Guid = servicePlanVisibilityRes.Meta.Guid
servicePlanVisibility.CreatedAt = servicePlanVisibilityRes.Meta.CreatedAt
servicePlanVisibility.UpdatedAt = servicePlanVisibilityRes.Meta.UpdatedAt
servicePlanVisibility.c = c
return servicePlanVisibility, nil
}

View File

@ -0,0 +1,129 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"github.com/pkg/errors"
)
type ServicePlansResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []ServicePlanResource `json:"resources"`
}
type ServicePlanResource struct {
Meta Meta `json:"metadata"`
Entity ServicePlan `json:"entity"`
}
type ServicePlan struct {
Name string `json:"name"`
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Free bool `json:"free"`
Description string `json:"description"`
ServiceGuid string `json:"service_guid"`
Extra interface{} `json:"extra"`
UniqueId string `json:"unique_id"`
Public bool `json:"public"`
Active bool `json:"active"`
Bindable bool `json:"bindable"`
ServiceUrl string `json:"service_url"`
ServiceInstancesUrl string `json:"service_instances_url"`
c *Client
}
func (c *Client) ListServicePlansByQuery(query url.Values) ([]ServicePlan, error) {
var servicePlans []ServicePlan
requestUrl := "/v2/service_plans?" + query.Encode()
for {
var servicePlansResp ServicePlansResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting service plans")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading service plans request:")
}
err = json.Unmarshal(resBody, &servicePlansResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling service plans")
}
for _, servicePlan := range servicePlansResp.Resources {
servicePlan.Entity.Guid = servicePlan.Meta.Guid
servicePlan.Entity.CreatedAt = servicePlan.Meta.CreatedAt
servicePlan.Entity.UpdatedAt = servicePlan.Meta.UpdatedAt
servicePlan.Entity.c = c
servicePlans = append(servicePlans, servicePlan.Entity)
}
requestUrl = servicePlansResp.NextUrl
if requestUrl == "" {
break
}
}
return servicePlans, nil
}
func (c *Client) ListServicePlans() ([]ServicePlan, error) {
return c.ListServicePlansByQuery(nil)
}
func (c *Client) GetServicePlanByGUID(guid string) (*ServicePlan, error) {
var (
plan *ServicePlan
planResponse ServicePlanResource
)
r := c.NewRequest("GET", "/v2/service_plans/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
err = json.Unmarshal(body, &planResponse)
if err != nil {
return nil, err
}
planResponse.Entity.Guid = planResponse.Meta.Guid
planResponse.Entity.CreatedAt = planResponse.Meta.CreatedAt
planResponse.Entity.UpdatedAt = planResponse.Meta.UpdatedAt
plan = &planResponse.Entity
return plan, nil
}
func (c *Client) MakeServicePlanPublic(servicePlanGUID string) error {
return c.setPlanGlobalVisibility(servicePlanGUID, true)
}
func (c *Client) MakeServicePlanPrivate(servicePlanGUID string) error {
return c.setPlanGlobalVisibility(servicePlanGUID, false)
}
func (c *Client) setPlanGlobalVisibility(servicePlanGUID string, public bool) error {
bodyString := fmt.Sprintf(`{"public": %t}`, public)
req := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/service_plans/%s", servicePlanGUID), bytes.NewBufferString(bodyString))
resp, err := c.DoRequest(req)
if err != nil {
return err
}
defer resp.Body.Close()
return nil
}

View File

@ -0,0 +1,72 @@
package cfclient
import (
"encoding/json"
"fmt"
"net/url"
"github.com/pkg/errors"
)
type ServiceUsageEvent struct {
GUID string `json:"guid"`
CreatedAt string `json:"created_at"`
State string `json:"state"`
OrgGUID string `json:"org_guid"`
SpaceGUID string `json:"space_guid"`
SpaceName string `json:"space_name"`
ServiceInstanceGUID string `json:"service_instance_guid"`
ServiceInstanceName string `json:"service_instance_name"`
ServiceInstanceType string `json:"service_instance_type"`
ServicePlanGUID string `json:"service_plan_guid"`
ServicePlanName string `json:"service_plan_name"`
ServiceGUID string `json:"service_guid"`
ServiceLabel string `json:"service_label"`
c *Client
}
type ServiceUsageEventsResponse struct {
TotalResults int `json:"total_results"`
Pages int `json:"total_pages"`
NextURL string `json:"next_url"`
Resources []ServiceUsageEventResource `json:"resources"`
}
type ServiceUsageEventResource struct {
Meta Meta `json:"metadata"`
Entity ServiceUsageEvent `json:"entity"`
}
// ListServiceUsageEventsByQuery lists all events matching the provided query.
func (c *Client) ListServiceUsageEventsByQuery(query url.Values) ([]ServiceUsageEvent, error) {
var serviceUsageEvents []ServiceUsageEvent
requestURL := fmt.Sprintf("/v2/service_usage_events?%s", query.Encode())
for {
var serviceUsageEventsResponse ServiceUsageEventsResponse
r := c.NewRequest("GET", requestURL)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "error requesting events")
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&serviceUsageEventsResponse); err != nil {
return nil, errors.Wrap(err, "error unmarshaling events")
}
for _, e := range serviceUsageEventsResponse.Resources {
e.Entity.GUID = e.Meta.Guid
e.Entity.CreatedAt = e.Meta.CreatedAt
e.Entity.c = c
serviceUsageEvents = append(serviceUsageEvents, e.Entity)
}
requestURL = serviceUsageEventsResponse.NextURL
if requestURL == "" {
break
}
}
return serviceUsageEvents, nil
}
// ListServiceUsageEvents lists all unfiltered events.
func (c *Client) ListServiceUsageEvents() ([]ServiceUsageEvent, error) {
return c.ListServiceUsageEventsByQuery(nil)
}

View File

@ -0,0 +1,107 @@
package cfclient
import (
"encoding/json"
"io/ioutil"
"net/url"
"github.com/pkg/errors"
)
type ServicesResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []ServicesResource `json:"resources"`
}
type ServicesResource struct {
Meta Meta `json:"metadata"`
Entity Service `json:"entity"`
}
type Service struct {
Guid string `json:"guid"`
Label string `json:"label"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Description string `json:"description"`
Active bool `json:"active"`
Bindable bool `json:"bindable"`
ServiceBrokerGuid string `json:"service_broker_guid"`
PlanUpdateable bool `json:"plan_updateable"`
Tags []string `json:"tags"`
UniqueID string `json:"unique_id"`
Extra string `json:"extra"`
Requires []string `json:"requires"`
InstancesRetrievable bool `json:"instances_retrievable"`
BindingsRetrievable bool `json:"bindings_retrievable"`
c *Client
}
type ServiceSummary struct {
Guid string `json:"guid"`
Name string `json:"name"`
BoundAppCount int `json:"bound_app_count"`
}
func (c *Client) GetServiceByGuid(guid string) (Service, error) {
var serviceRes ServicesResource
r := c.NewRequest("GET", "/v2/services/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return Service{}, err
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return Service{}, err
}
err = json.Unmarshal(body, &serviceRes)
if err != nil {
return Service{}, err
}
serviceRes.Entity.Guid = serviceRes.Meta.Guid
serviceRes.Entity.CreatedAt = serviceRes.Meta.CreatedAt
serviceRes.Entity.UpdatedAt = serviceRes.Meta.UpdatedAt
return serviceRes.Entity, nil
}
func (c *Client) ListServicesByQuery(query url.Values) ([]Service, error) {
var services []Service
requestUrl := "/v2/services?" + query.Encode()
for {
var serviceResp ServicesResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting services")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading services request:")
}
err = json.Unmarshal(resBody, &serviceResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling services")
}
for _, service := range serviceResp.Resources {
service.Entity.Guid = service.Meta.Guid
service.Entity.CreatedAt = service.Meta.CreatedAt
service.Entity.UpdatedAt = service.Meta.UpdatedAt
service.Entity.c = c
services = append(services, service.Entity)
}
requestUrl = serviceResp.NextUrl
if requestUrl == "" {
break
}
}
return services, nil
}
func (c *Client) ListServices() ([]Service, error) {
return c.ListServicesByQuery(nil)
}

View File

@ -0,0 +1,183 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type SpaceQuotasResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []SpaceQuotasResource `json:"resources"`
}
type SpaceQuotasResource struct {
Meta Meta `json:"metadata"`
Entity SpaceQuota `json:"entity"`
}
type SpaceQuotaRequest struct {
Name string `json:"name"`
OrganizationGuid string `json:"organization_guid"`
NonBasicServicesAllowed bool `json:"non_basic_services_allowed"`
TotalServices int `json:"total_services"`
TotalRoutes int `json:"total_routes"`
MemoryLimit int `json:"memory_limit"`
InstanceMemoryLimit int `json:"instance_memory_limit"`
AppInstanceLimit int `json:"app_instance_limit"`
AppTaskLimit int `json:"app_task_limit"`
TotalServiceKeys int `json:"total_service_keys"`
TotalReservedRoutePorts int `json:"total_reserved_route_ports"`
}
type SpaceQuota struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Name string `json:"name"`
OrganizationGuid string `json:"organization_guid"`
NonBasicServicesAllowed bool `json:"non_basic_services_allowed"`
TotalServices int `json:"total_services"`
TotalRoutes int `json:"total_routes"`
MemoryLimit int `json:"memory_limit"`
InstanceMemoryLimit int `json:"instance_memory_limit"`
AppInstanceLimit int `json:"app_instance_limit"`
AppTaskLimit int `json:"app_task_limit"`
TotalServiceKeys int `json:"total_service_keys"`
TotalReservedRoutePorts int `json:"total_reserved_route_ports"`
c *Client
}
func (c *Client) ListSpaceQuotasByQuery(query url.Values) ([]SpaceQuota, error) {
var spaceQuotas []SpaceQuota
requestUrl := "/v2/space_quota_definitions?" + query.Encode()
for {
spaceQuotasResp, err := c.getSpaceQuotasResponse(requestUrl)
if err != nil {
return []SpaceQuota{}, err
}
for _, space := range spaceQuotasResp.Resources {
space.Entity.Guid = space.Meta.Guid
space.Entity.CreatedAt = space.Meta.CreatedAt
space.Entity.UpdatedAt = space.Meta.UpdatedAt
space.Entity.c = c
spaceQuotas = append(spaceQuotas, space.Entity)
}
requestUrl = spaceQuotasResp.NextUrl
if requestUrl == "" {
break
}
}
return spaceQuotas, nil
}
func (c *Client) ListSpaceQuotas() ([]SpaceQuota, error) {
return c.ListSpaceQuotasByQuery(nil)
}
func (c *Client) GetSpaceQuotaByName(name string) (SpaceQuota, error) {
q := url.Values{}
q.Set("q", "name:"+name)
spaceQuotas, err := c.ListSpaceQuotasByQuery(q)
if err != nil {
return SpaceQuota{}, err
}
if len(spaceQuotas) != 1 {
return SpaceQuota{}, fmt.Errorf("Unable to find space quota " + name)
}
return spaceQuotas[0], nil
}
func (c *Client) getSpaceQuotasResponse(requestUrl string) (SpaceQuotasResponse, error) {
var spaceQuotasResp SpaceQuotasResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return SpaceQuotasResponse{}, errors.Wrap(err, "Error requesting space quotas")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return SpaceQuotasResponse{}, errors.Wrap(err, "Error reading space quotas body")
}
err = json.Unmarshal(resBody, &spaceQuotasResp)
if err != nil {
return SpaceQuotasResponse{}, errors.Wrap(err, "Error unmarshalling space quotas")
}
return spaceQuotasResp, nil
}
func (c *Client) AssignSpaceQuota(quotaGUID, spaceGUID string) error {
//Perform the PUT and check for errors
resp, err := c.DoRequest(c.NewRequest("PUT", fmt.Sprintf("/v2/space_quota_definitions/%s/spaces/%s", quotaGUID, spaceGUID)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusCreated { //201
return fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return nil
}
func (c *Client) CreateSpaceQuota(spaceQuote SpaceQuotaRequest) (*SpaceQuota, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(spaceQuote)
if err != nil {
return nil, err
}
r := c.NewRequestWithBody("POST", "/v2/space_quota_definitions", buf)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleSpaceQuotaResp(resp)
}
func (c *Client) UpdateSpaceQuota(spaceQuotaGUID string, spaceQuote SpaceQuotaRequest) (*SpaceQuota, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(spaceQuote)
if err != nil {
return nil, err
}
r := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/space_quota_definitions/%s", spaceQuotaGUID), buf)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleSpaceQuotaResp(resp)
}
func (c *Client) handleSpaceQuotaResp(resp *http.Response) (*SpaceQuota, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
var spaceQuotasResource SpaceQuotasResource
err = json.Unmarshal(body, &spaceQuotasResource)
if err != nil {
return nil, err
}
return c.mergeSpaceQuotaResource(spaceQuotasResource), nil
}
func (c *Client) mergeSpaceQuotaResource(spaceQuote SpaceQuotasResource) *SpaceQuota {
spaceQuote.Entity.Guid = spaceQuote.Meta.Guid
spaceQuote.Entity.CreatedAt = spaceQuote.Meta.CreatedAt
spaceQuote.Entity.UpdatedAt = spaceQuote.Meta.UpdatedAt
spaceQuote.Entity.c = c
return &spaceQuote.Entity
}

View File

@ -0,0 +1,790 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"github.com/pkg/errors"
)
type SpaceRequest struct {
Name string `json:"name"`
OrganizationGuid string `json:"organization_guid"`
DeveloperGuid []string `json:"developer_guids,omitempty"`
ManagerGuid []string `json:"manager_guids,omitempty"`
AuditorGuid []string `json:"auditor_guids,omitempty"`
DomainGuid []string `json:"domain_guids,omitempty"`
SecurityGroupGuids []string `json:"security_group_guids,omitempty"`
SpaceQuotaDefGuid string `json:"space_quota_definition_guid,omitempty"`
IsolationSegmentGuid string `json:"isolation_segment_guid,omitempty"`
AllowSSH bool `json:"allow_ssh"`
}
type SpaceResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []SpaceResource `json:"resources"`
}
type SpaceResource struct {
Meta Meta `json:"metadata"`
Entity Space `json:"entity"`
}
type ServicePlanEntity struct {
Name string `json:"name"`
Free bool `json:"free"`
Public bool `json:"public"`
Active bool `json:"active"`
Description string `json:"description"`
ServiceOfferingGUID string `json:"service_guid"`
ServiceOffering ServiceOfferingResource `json:"service"`
}
type ServiceOfferingExtra struct {
DisplayName string `json:"displayName"`
DocumentationURL string `json:"documentationURL"`
LongDescription string `json:"longDescription"`
}
type ServiceOfferingEntity struct {
Label string
Description string
Provider string `json:"provider"`
BrokerGUID string `json:"service_broker_guid"`
Requires []string `json:"requires"`
ServicePlans []interface{} `json:"service_plans"`
Extra ServiceOfferingExtra
}
type ServiceOfferingResource struct {
Metadata Meta
Entity ServiceOfferingEntity
}
type ServiceOfferingResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
PrevUrl string `json:"prev_url"`
Resources []ServiceOfferingResource `json:"resources"`
}
type SpaceUserResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextURL string `json:"next_url"`
Resources []UserResource `json:"resources"`
}
type Space struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Name string `json:"name"`
OrganizationGuid string `json:"organization_guid"`
OrgURL string `json:"organization_url"`
OrgData OrgResource `json:"organization"`
QuotaDefinitionGuid string `json:"space_quota_definition_guid"`
IsolationSegmentGuid string `json:"isolation_segment_guid"`
AllowSSH bool `json:"allow_ssh"`
c *Client
}
type SpaceSummary struct {
Guid string `json:"guid"`
Name string `json:"name"`
Apps []AppSummary `json:"apps"`
Services []ServiceSummary `json:"services"`
}
type SpaceRoleResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []SpaceRoleResource `json:"resources"`
}
type SpaceRoleResource struct {
Meta Meta `json:"metadata"`
Entity SpaceRole `json:"entity"`
}
type SpaceRole struct {
Guid string `json:"guid"`
Admin bool `json:"admin"`
Active bool `json:"active"`
DefaultSpaceGuid string `json:"default_space_guid"`
Username string `json:"username"`
SpaceRoles []string `json:"space_roles"`
SpacesUrl string `json:"spaces_url"`
OrganizationsUrl string `json:"organizations_url"`
ManagedOrganizationsUrl string `json:"managed_organizations_url"`
BillingManagedOrganizationsUrl string `json:"billing_managed_organizations_url"`
AuditedOrganizationsUrl string `json:"audited_organizations_url"`
ManagedSpacesUrl string `json:"managed_spaces_url"`
AuditedSpacesUrl string `json:"audited_spaces_url"`
c *Client
}
func (s *Space) Org() (Org, error) {
var orgResource OrgResource
r := s.c.NewRequest("GET", s.OrgURL)
resp, err := s.c.DoRequest(r)
if err != nil {
return Org{}, errors.Wrap(err, "Error requesting org")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Org{}, errors.Wrap(err, "Error reading org request")
}
err = json.Unmarshal(resBody, &orgResource)
if err != nil {
return Org{}, errors.Wrap(err, "Error unmarshaling org")
}
return s.c.mergeOrgResource(orgResource), nil
}
func (s *Space) Quota() (*SpaceQuota, error) {
var spaceQuota *SpaceQuota
var spaceQuotaResource SpaceQuotasResource
if s.QuotaDefinitionGuid == "" {
return nil, nil
}
requestUrl := fmt.Sprintf("/v2/space_quota_definitions/%s", s.QuotaDefinitionGuid)
r := s.c.NewRequest("GET", requestUrl)
resp, err := s.c.DoRequest(r)
if err != nil {
return &SpaceQuota{}, errors.Wrap(err, "Error requesting space quota")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return &SpaceQuota{}, errors.Wrap(err, "Error reading space quota body")
}
err = json.Unmarshal(resBody, &spaceQuotaResource)
if err != nil {
return &SpaceQuota{}, errors.Wrap(err, "Error unmarshalling space quota")
}
spaceQuota = &spaceQuotaResource.Entity
spaceQuota.Guid = spaceQuotaResource.Meta.Guid
spaceQuota.c = s.c
return spaceQuota, nil
}
func (s *Space) Summary() (SpaceSummary, error) {
var spaceSummary SpaceSummary
requestUrl := fmt.Sprintf("/v2/spaces/%s/summary", s.Guid)
r := s.c.NewRequest("GET", requestUrl)
resp, err := s.c.DoRequest(r)
if err != nil {
return SpaceSummary{}, errors.Wrap(err, "Error requesting space summary")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return SpaceSummary{}, errors.Wrap(err, "Error reading space summary body")
}
err = json.Unmarshal(resBody, &spaceSummary)
if err != nil {
return SpaceSummary{}, errors.Wrap(err, "Error unmarshalling space summary")
}
return spaceSummary, nil
}
func (s *Space) Roles() ([]SpaceRole, error) {
var roles []SpaceRole
requestUrl := fmt.Sprintf("/v2/spaces/%s/user_roles", s.Guid)
for {
rolesResp, err := s.c.getSpaceRolesResponse(requestUrl)
if err != nil {
return roles, err
}
for _, role := range rolesResp.Resources {
role.Entity.Guid = role.Meta.Guid
role.Entity.c = s.c
roles = append(roles, role.Entity)
}
requestUrl = rolesResp.NextUrl
if requestUrl == "" {
break
}
}
return roles, nil
}
func (c *Client) CreateSpace(req SpaceRequest) (Space, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return Space{}, err
}
r := c.NewRequestWithBody("POST", "/v2/spaces", buf)
resp, err := c.DoRequest(r)
if err != nil {
return Space{}, err
}
if resp.StatusCode != http.StatusCreated {
return Space{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleSpaceResp(resp)
}
func (c *Client) UpdateSpace(spaceGUID string, req SpaceRequest) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.Update(req)
}
func (c *Client) DeleteSpace(guid string, recursive, async bool) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/spaces/%s?recursive=%t&async=%t", guid, recursive, async)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting space %s, response code: %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) ListSpaceManagersByQuery(spaceGUID string, query url.Values) ([]User, error) {
return c.listSpaceUsersByRoleAndQuery(spaceGUID, "managers", query)
}
func (c *Client) ListSpaceManagers(spaceGUID string) ([]User, error) {
return c.ListSpaceManagersByQuery(spaceGUID, nil)
}
func (c *Client) ListSpaceAuditorsByQuery(spaceGUID string, query url.Values) ([]User, error) {
return c.listSpaceUsersByRoleAndQuery(spaceGUID, "auditors", query)
}
func (c *Client) ListSpaceAuditors(spaceGUID string) ([]User, error) {
return c.ListSpaceAuditorsByQuery(spaceGUID, nil)
}
func (c *Client) ListSpaceDevelopersByQuery(spaceGUID string, query url.Values) ([]User, error) {
return c.listSpaceUsersByRoleAndQuery(spaceGUID, "developers", query)
}
func (c *Client) listSpaceUsersByRoleAndQuery(spaceGUID, role string, query url.Values) ([]User, error) {
var users []User
requestURL := fmt.Sprintf("/v2/spaces/%s/%s?%s", spaceGUID, role, query.Encode())
for {
userResp, err := c.getUserResponse(requestURL)
if err != nil {
return []User{}, err
}
for _, u := range userResp.Resources {
users = append(users, c.mergeUserResource(u))
}
requestURL = userResp.NextUrl
if requestURL == "" {
break
}
}
return users, nil
}
func (c *Client) ListSpaceDevelopers(spaceGUID string) ([]User, error) {
return c.ListSpaceDevelopersByQuery(spaceGUID, nil)
}
func (c *Client) AssociateSpaceDeveloper(spaceGUID, userGUID string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateDeveloper(userGUID)
}
func (c *Client) AssociateSpaceDeveloperByUsername(spaceGUID, name string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateDeveloperByUsername(name)
}
func (c *Client) AssociateSpaceDeveloperByUsernameAndOrigin(spaceGUID, name, origin string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateDeveloperByUsernameAndOrigin(name, origin)
}
func (c *Client) RemoveSpaceDeveloper(spaceGUID, userGUID string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveDeveloper(userGUID)
}
func (c *Client) RemoveSpaceDeveloperByUsername(spaceGUID, name string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveDeveloperByUsername(name)
}
func (c *Client) RemoveSpaceDeveloperByUsernameAndOrigin(spaceGUID, name, origin string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveDeveloperByUsernameAndOrigin(name, origin)
}
func (c *Client) AssociateSpaceAuditor(spaceGUID, userGUID string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateAuditor(userGUID)
}
func (c *Client) AssociateSpaceAuditorByUsername(spaceGUID, name string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateAuditorByUsername(name)
}
func (c *Client) AssociateSpaceAuditorByUsernameAndOrigin(spaceGUID, name, origin string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateAuditorByUsernameAndOrigin(name, origin)
}
func (c *Client) RemoveSpaceAuditor(spaceGUID, userGUID string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveAuditor(userGUID)
}
func (c *Client) RemoveSpaceAuditorByUsername(spaceGUID, name string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveAuditorByUsername(name)
}
func (c *Client) RemoveSpaceAuditorByUsernameAndOrigin(spaceGUID, name, origin string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveAuditorByUsernameAndOrigin(name, origin)
}
func (c *Client) AssociateSpaceManager(spaceGUID, userGUID string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateManager(userGUID)
}
func (c *Client) AssociateSpaceManagerByUsername(spaceGUID, name string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateManagerByUsername(name)
}
func (c *Client) AssociateSpaceManagerByUsernameAndOrigin(spaceGUID, name, origin string) (Space, error) {
space := Space{Guid: spaceGUID, c: c}
return space.AssociateManagerByUsernameAndOrigin(name, origin)
}
func (c *Client) RemoveSpaceManager(spaceGUID, userGUID string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveManager(userGUID)
}
func (c *Client) RemoveSpaceManagerByUsername(spaceGUID, name string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveManagerByUsername(name)
}
func (c *Client) RemoveSpaceManagerByUsernameAndOrigin(spaceGUID, name, origin string) error {
space := Space{Guid: spaceGUID, c: c}
return space.RemoveManagerByUsernameAndOrigin(name, origin)
}
func (s *Space) AssociateDeveloper(userGUID string) (Space, error) {
return s.associateRole(userGUID, "developers")
}
func (s *Space) AssociateDeveloperByUsername(name string) (Space, error) {
return s.associateUserByRole(name, "developers", "")
}
func (s *Space) AssociateDeveloperByUsernameAndOrigin(name, origin string) (Space, error) {
return s.associateUserByRole(name, "developers", origin)
}
func (s *Space) RemoveDeveloper(userGUID string) error {
return s.removeRole(userGUID, "developers")
}
func (s *Space) RemoveDeveloperByUsername(name string) error {
return s.removeUserByRole(name, "developers", "")
}
func (s *Space) RemoveDeveloperByUsernameAndOrigin(name, origin string) error {
return s.removeUserByRole(name, "developers", origin)
}
func (s *Space) AssociateAuditor(userGUID string) (Space, error) {
return s.associateRole(userGUID, "auditors")
}
func (s *Space) AssociateAuditorByUsername(name string) (Space, error) {
return s.associateUserByRole(name, "auditors", "")
}
func (s *Space) AssociateAuditorByUsernameAndOrigin(name, origin string) (Space, error) {
return s.associateUserByRole(name, "auditors", origin)
}
func (s *Space) RemoveAuditor(userGUID string) error {
return s.removeRole(userGUID, "auditors")
}
func (s *Space) RemoveAuditorByUsername(name string) error {
return s.removeUserByRole(name, "auditors", "")
}
func (s *Space) RemoveAuditorByUsernameAndOrigin(name, origin string) error {
return s.removeUserByRole(name, "auditors", origin)
}
func (s *Space) AssociateManager(userGUID string) (Space, error) {
return s.associateRole(userGUID, "managers")
}
func (s *Space) AssociateManagerByUsername(name string) (Space, error) {
return s.associateUserByRole(name, "managers", "")
}
func (s *Space) AssociateManagerByUsernameAndOrigin(name, origin string) (Space, error) {
return s.associateUserByRole(name, "managers", origin)
}
func (s *Space) RemoveManager(userGUID string) error {
return s.removeRole(userGUID, "managers")
}
func (s *Space) RemoveManagerByUsername(name string) error {
return s.removeUserByRole(name, "managers", "")
}
func (s *Space) RemoveManagerByUsernameAndOrigin(name, origin string) error {
return s.removeUserByRole(name, "managers", origin)
}
func (s *Space) associateRole(userGUID, role string) (Space, error) {
requestUrl := fmt.Sprintf("/v2/spaces/%s/%s/%s", s.Guid, role, userGUID)
r := s.c.NewRequest("PUT", requestUrl)
resp, err := s.c.DoRequest(r)
if err != nil {
return Space{}, err
}
if resp.StatusCode != http.StatusCreated {
return Space{}, errors.Wrapf(err, "Error associating %s %s, response code: %d", role, userGUID, resp.StatusCode)
}
return s.c.handleSpaceResp(resp)
}
func (s *Space) associateUserByRole(name, role, origin string) (Space, error) {
requestUrl := fmt.Sprintf("/v2/spaces/%s/%s", s.Guid, role)
buf := bytes.NewBuffer(nil)
payload := make(map[string]string)
payload["username"] = name
if origin != "" {
payload["origin"] = origin
}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return Space{}, err
}
r := s.c.NewRequestWithBody("PUT", requestUrl, buf)
resp, err := s.c.DoRequest(r)
if err != nil {
return Space{}, err
}
if resp.StatusCode != http.StatusCreated {
return Space{}, errors.Wrapf(err, "Error associating %s %s, response code: %d", role, name, resp.StatusCode)
}
return s.c.handleSpaceResp(resp)
}
func (s *Space) removeRole(userGUID, role string) error {
requestUrl := fmt.Sprintf("/v2/spaces/%s/%s/%s", s.Guid, role, userGUID)
r := s.c.NewRequest("DELETE", requestUrl)
resp, err := s.c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error removing %s %s, response code: %d", role, userGUID, resp.StatusCode)
}
return nil
}
func (s *Space) removeUserByRole(name, role, origin string) error {
var requestURL string
var method string
buf := bytes.NewBuffer(nil)
payload := make(map[string]string)
payload["username"] = name
if origin != "" {
payload["origin"] = origin
requestURL = fmt.Sprintf("/v2/spaces/%s/%s/remove", s.Guid, role)
method = "POST"
} else {
requestURL = fmt.Sprintf("/v2/spaces/%s/%s", s.Guid, role)
method = "DELETE"
}
err := json.NewEncoder(buf).Encode(payload)
if err != nil {
return err
}
r := s.c.NewRequestWithBody(method, requestURL, buf)
resp, err := s.c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.Wrapf(err, "Error removing %s %s, response code: %d", role, name, resp.StatusCode)
}
return nil
}
func (c *Client) ListSpaceSecGroups(spaceGUID string) (secGroups []SecGroup, err error) {
space := Space{Guid: spaceGUID, c: c}
return space.ListSecGroups()
}
func (s *Space) ListSecGroups() (secGroups []SecGroup, err error) {
requestURL := fmt.Sprintf("/v2/spaces/%s/security_groups?inline-relations-depth=1", s.Guid)
for requestURL != "" {
var secGroupResp SecGroupResponse
r := s.c.NewRequest("GET", requestURL)
resp, err := s.c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting sec groups")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading sec group response body")
}
err = json.Unmarshal(resBody, &secGroupResp)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling sec group")
}
for _, secGroup := range secGroupResp.Resources {
secGroup.Entity.Guid = secGroup.Meta.Guid
secGroup.Entity.c = s.c
for i, space := range secGroup.Entity.SpacesData {
space.Entity.Guid = space.Meta.Guid
secGroup.Entity.SpacesData[i] = space
}
if len(secGroup.Entity.SpacesData) == 0 {
spaces, err := secGroup.Entity.ListSpaceResources()
if err != nil {
return nil, err
}
for _, space := range spaces {
secGroup.Entity.SpacesData = append(secGroup.Entity.SpacesData, space)
}
}
secGroups = append(secGroups, secGroup.Entity)
}
requestURL = secGroupResp.NextUrl
resp.Body.Close()
}
return secGroups, nil
}
func (s *Space) GetServiceOfferings() (ServiceOfferingResponse, error) {
var response ServiceOfferingResponse
requestURL := fmt.Sprintf("/v2/spaces/%s/services", s.Guid)
req := s.c.NewRequest("GET", requestURL)
resp, err := s.c.DoRequest(req)
if err != nil {
return ServiceOfferingResponse{}, errors.Wrap(err, "Error requesting service offerings")
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return ServiceOfferingResponse{}, errors.Wrap(err, "Error reading service offering response")
}
err = json.Unmarshal(body, &response)
if err != nil {
return ServiceOfferingResponse{}, errors.Wrap(err, "Error unmarshalling service offering response")
}
return response, nil
}
func (s *Space) Update(req SpaceRequest) (Space, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return Space{}, err
}
r := s.c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/spaces/%s", s.Guid), buf)
resp, err := s.c.DoRequest(r)
if err != nil {
return Space{}, err
}
if resp.StatusCode != http.StatusCreated {
return Space{}, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return s.c.handleSpaceResp(resp)
}
func (c *Client) ListSpacesByQuery(query url.Values) ([]Space, error) {
return c.fetchSpaces("/v2/spaces?" + query.Encode())
}
func (c *Client) ListSpaces() ([]Space, error) {
return c.ListSpacesByQuery(nil)
}
func (c *Client) fetchSpaces(requestUrl string) ([]Space, error) {
var spaces []Space
for {
spaceResp, err := c.getSpaceResponse(requestUrl)
if err != nil {
return []Space{}, err
}
for _, space := range spaceResp.Resources {
spaces = append(spaces, c.mergeSpaceResource(space))
}
requestUrl = spaceResp.NextUrl
if requestUrl == "" {
break
}
}
return spaces, nil
}
func (c *Client) GetSpaceByName(spaceName string, orgGuid string) (space Space, err error) {
query := url.Values{}
query.Add("q", fmt.Sprintf("organization_guid:%s", orgGuid))
query.Add("q", fmt.Sprintf("name:%s", spaceName))
spaces, err := c.ListSpacesByQuery(query)
if err != nil {
return
}
if len(spaces) == 0 {
return space, fmt.Errorf("No space found with name: `%s` in org with GUID: `%s`", spaceName, orgGuid)
}
return spaces[0], nil
}
func (c *Client) GetSpaceByGuid(spaceGUID string) (Space, error) {
requestUrl := fmt.Sprintf("/v2/spaces/%s", spaceGUID)
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return Space{}, errors.Wrap(err, "Error requesting space info")
}
return c.handleSpaceResp(resp)
}
func (c *Client) getSpaceResponse(requestUrl string) (SpaceResponse, error) {
var spaceResp SpaceResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return SpaceResponse{}, errors.Wrap(err, "Error requesting spaces")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return SpaceResponse{}, errors.Wrap(err, "Error reading space request")
}
err = json.Unmarshal(resBody, &spaceResp)
if err != nil {
return SpaceResponse{}, errors.Wrap(err, "Error unmarshalling space")
}
return spaceResp, nil
}
func (c *Client) getSpaceRolesResponse(requestUrl string) (SpaceRoleResponse, error) {
var roleResp SpaceRoleResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return roleResp, errors.Wrap(err, "Error requesting space roles")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return roleResp, errors.Wrap(err, "Error reading space roles request")
}
err = json.Unmarshal(resBody, &roleResp)
if err != nil {
return roleResp, errors.Wrap(err, "Error unmarshalling space roles")
}
return roleResp, nil
}
func (c *Client) handleSpaceResp(resp *http.Response) (Space, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return Space{}, err
}
var spaceResource SpaceResource
err = json.Unmarshal(body, &spaceResource)
if err != nil {
return Space{}, err
}
return c.mergeSpaceResource(spaceResource), nil
}
func (c *Client) mergeSpaceResource(space SpaceResource) Space {
space.Entity.Guid = space.Meta.Guid
space.Entity.CreatedAt = space.Meta.CreatedAt
space.Entity.UpdatedAt = space.Meta.UpdatedAt
space.Entity.c = c
return space.Entity
}
type serviceOfferingExtra ServiceOfferingExtra
func (resource *ServiceOfferingExtra) UnmarshalJSON(rawData []byte) error {
if string(rawData) == "null" {
return nil
}
extra := serviceOfferingExtra{}
unquoted, err := strconv.Unquote(string(rawData))
if err != nil {
return err
}
err = json.Unmarshal([]byte(unquoted), &extra)
if err != nil {
return err
}
*resource = ServiceOfferingExtra(extra)
return nil
}
func (c *Client) IsolationSegmentForSpace(spaceGUID, isolationSegmentGUID string) error {
return c.updateSpaceIsolationSegment(spaceGUID, map[string]interface{}{"guid": isolationSegmentGUID})
}
func (c *Client) ResetIsolationSegmentForSpace(spaceGUID string) error {
return c.updateSpaceIsolationSegment(spaceGUID, nil)
}
func (c *Client) updateSpaceIsolationSegment(spaceGUID string, data interface{}) error {
requestURL := fmt.Sprintf("/v3/spaces/%s/relationships/isolation_segment", spaceGUID)
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(map[string]interface{}{"data": data})
if err != nil {
return err
}
r := c.NewRequestWithBody("PATCH", requestURL, buf)
resp, err := c.DoRequest(r)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK {
return errors.Wrapf(err, "Error setting isolation segment for space %s, response code: %d", spaceGUID, resp.StatusCode)
}
return nil
}

View File

@ -0,0 +1,76 @@
package cfclient
import (
"encoding/json"
"io/ioutil"
"net/url"
"github.com/pkg/errors"
)
type StacksResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []StacksResource `json:"resources"`
}
type StacksResource struct {
Meta Meta `json:"metadata"`
Entity Stack `json:"entity"`
}
type Stack struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Description string `json:"description"`
c *Client
}
func (c *Client) ListStacksByQuery(query url.Values) ([]Stack, error) {
var stacks []Stack
requestUrl := "/v2/stacks?" + query.Encode()
for {
stacksResp, err := c.getStacksResponse(requestUrl)
if err != nil {
return []Stack{}, err
}
for _, stack := range stacksResp.Resources {
stack.Entity.Guid = stack.Meta.Guid
stack.Entity.CreatedAt = stack.Meta.CreatedAt
stack.Entity.UpdatedAt = stack.Meta.UpdatedAt
stack.Entity.c = c
stacks = append(stacks, stack.Entity)
}
requestUrl = stacksResp.NextUrl
if requestUrl == "" {
break
}
}
return stacks, nil
}
func (c *Client) ListStacks() ([]Stack, error) {
return c.ListStacksByQuery(nil)
}
func (c *Client) getStacksResponse(requestUrl string) (StacksResponse, error) {
var stacksResp StacksResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return StacksResponse{}, errors.Wrap(err, "Error requesting stacks")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return StacksResponse{}, errors.Wrap(err, "Error reading stacks body")
}
err = json.Unmarshal(resBody, &stacksResp)
if err != nil {
return StacksResponse{}, errors.Wrap(err, "Error unmarshalling stacks")
}
return stacksResp, nil
}

View File

@ -0,0 +1,204 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/url"
"time"
"github.com/pkg/errors"
)
// TaskListResponse is the JSON response from the API.
type TaskListResponse struct {
Pagination Pagination `json:"pagination"`
Tasks []Task `json:"resources"`
}
// Task is a description of a task element.
type Task struct {
GUID string `json:"guid"`
SequenceID int `json:"sequence_id"`
Name string `json:"name"`
Command string `json:"command"`
State string `json:"state"`
MemoryInMb int `json:"memory_in_mb"`
DiskInMb int `json:"disk_in_mb"`
Result struct {
FailureReason string `json:"failure_reason"`
} `json:"result"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DropletGUID string `json:"droplet_guid"`
Links struct {
Self Link `json:"self"`
App Link `json:"app"`
Droplet Link `json:"droplet"`
} `json:"links"`
}
// TaskRequest is a v3 JSON object as described in:
// http://v3-apidocs.cloudfoundry.org/version/3.0.0/index.html#create-a-task
type TaskRequest struct {
Command string `json:"command"`
Name string `json:"name"`
MemoryInMegabyte int `json:"memory_in_mb"`
DiskInMegabyte int `json:"disk_in_mb"`
DropletGUID string `json:"droplet_guid"`
}
func (c *Client) makeTaskListRequestWithParams(baseUrl string, query url.Values) ([]byte, error) {
requestUrl := baseUrl + "?" + query.Encode()
req := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(req)
if err != nil {
return nil, errors.Wrap(err, "Error requesting tasks")
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.Wrapf(err, "Error requesting tasks: status code not 200, it was %d", resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
}
func parseTaskListRespones(answer []byte) (TaskListResponse, error) {
var response TaskListResponse
err := json.Unmarshal(answer, &response)
if err != nil {
return response, errors.Wrap(err, "Error unmarshaling response %v")
}
return response, nil
}
func (c *Client) handleTasksApiCall(apiUrl string, query url.Values) ([]Task, error) {
body, err := c.makeTaskListRequestWithParams(apiUrl, query)
if err != nil {
return nil, errors.Wrap(err, "Error requesting tasks")
}
response, err := parseTaskListRespones(body)
if err != nil {
return nil, errors.Wrap(err, "Error reading tasks")
}
return response.Tasks, nil
}
// ListTasks returns all tasks the user has access to.
// See http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks
func (c *Client) ListTasks() ([]Task, error) {
return c.handleTasksApiCall("/v3/tasks", url.Values{})
}
// ListTasksByQuery returns all tasks the user has access to, with query parameters.
// See http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks
func (c *Client) ListTasksByQuery(query url.Values) ([]Task, error) {
return c.handleTasksApiCall("/v3/tasks", query)
}
// TasksByApp returns task structures which aligned to an app identified by the given guid.
// See: http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks-for-an-app
func (c *Client) TasksByApp(guid string) ([]Task, error) {
return c.TasksByAppByQuery(guid, url.Values{})
}
// TasksByAppByQuery returns task structures which aligned to an app identified by the given guid
// and filtered by the given query parameters.
// See: http://v3-apidocs.cloudfoundry.org/version/3.12.0/index.html#list-tasks-for-an-app
func (c *Client) TasksByAppByQuery(guid string, query url.Values) ([]Task, error) {
uri := fmt.Sprintf("/v3/apps/%s/tasks", guid)
return c.handleTasksApiCall(uri, query)
}
func createReader(tr TaskRequest) (io.Reader, error) {
rmap := make(map[string]string)
rmap["command"] = tr.Command
if tr.Name != "" {
rmap["name"] = tr.Name
}
// setting droplet GUID causing issues
if tr.MemoryInMegabyte != 0 {
rmap["memory_in_mb"] = fmt.Sprintf("%d", tr.MemoryInMegabyte)
}
if tr.DiskInMegabyte != 0 {
rmap["disk_in_mb"] = fmt.Sprintf("%d", tr.DiskInMegabyte)
}
bodyReader := bytes.NewBuffer(nil)
enc := json.NewEncoder(bodyReader)
if err := enc.Encode(rmap); err != nil {
return nil, errors.Wrap(err, "Error during encoding task request")
}
return bodyReader, nil
}
// CreateTask creates a new task in CF system and returns its structure.
func (c *Client) CreateTask(tr TaskRequest) (task Task, err error) {
bodyReader, err := createReader(tr)
if err != nil {
return task, err
}
request := fmt.Sprintf("/v3/apps/%s/tasks", tr.DropletGUID)
req := c.NewRequestWithBody("POST", request, bodyReader)
resp, err := c.DoRequest(req)
if err != nil {
return task, errors.Wrap(err, "Error creating task")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return task, errors.Wrap(err, "Error reading task after creation")
}
err = json.Unmarshal(body, &task)
if err != nil {
return task, errors.Wrap(err, "Error unmarshaling task")
}
return task, err
}
// GetTaskByGuid returns a task structure by requesting it with the tasks GUID.
func (c *Client) GetTaskByGuid(guid string) (task Task, err error) {
request := fmt.Sprintf("/v3/tasks/%s", guid)
req := c.NewRequest("GET", request)
resp, err := c.DoRequest(req)
if err != nil {
return task, errors.Wrap(err, "Error requesting task")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return task, errors.Wrap(err, "Error reading task")
}
err = json.Unmarshal(body, &task)
if err != nil {
return task, errors.Wrap(err, "Error unmarshaling task")
}
return task, err
}
func (c *Client) TaskByGuid(guid string) (task Task, err error) {
return c.GetTaskByGuid(guid)
}
// TerminateTask cancels a task identified by its GUID.
func (c *Client) TerminateTask(guid string) error {
req := c.NewRequest("PUT", fmt.Sprintf("/v3/tasks/%s/cancel", guid))
resp, err := c.DoRequest(req)
if err != nil {
return errors.Wrap(err, "Error terminating task")
}
defer resp.Body.Close()
if resp.StatusCode != 202 {
return errors.Wrapf(err, "Failed terminating task, response status code %d", resp.StatusCode)
}
return nil
}

View File

@ -0,0 +1,8 @@
package cfclient
type Meta struct {
Guid string `json:"guid"`
Url string `json:"url"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}

View File

@ -0,0 +1,185 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type UserProvidedServiceInstancesResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []UserProvidedServiceInstanceResource `json:"resources"`
}
type UserProvidedServiceInstanceResource struct {
Meta Meta `json:"metadata"`
Entity UserProvidedServiceInstance `json:"entity"`
}
type UserProvidedServiceInstance struct {
Guid string `json:"guid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Credentials map[string]interface{} `json:"credentials"`
SpaceGuid string `json:"space_guid"`
Type string `json:"type"`
Tags []string `json:"tags"`
SpaceUrl string `json:"space_url"`
ServiceBindingsUrl string `json:"service_bindings_url"`
RoutesUrl string `json:"routes_url"`
RouteServiceUrl string `json:"route_service_url"`
SyslogDrainUrl string `json:"syslog_drain_url"`
c *Client
}
type UserProvidedServiceInstanceRequest struct {
Name string `json:"name"`
Credentials map[string]interface{} `json:"credentials"`
SpaceGuid string `json:"space_guid"`
Tags []string `json:"tags"`
RouteServiceUrl string `json:"route_service_url"`
SyslogDrainUrl string `json:"syslog_drain_url"`
}
func (c *Client) ListUserProvidedServiceInstancesByQuery(query url.Values) ([]UserProvidedServiceInstance, error) {
var instances []UserProvidedServiceInstance
requestUrl := "/v2/user_provided_service_instances?" + query.Encode()
for {
var sir UserProvidedServiceInstancesResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return nil, errors.Wrap(err, "Error requesting user provided service instances")
}
resBody, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "Error reading user provided service instances request:")
}
err = json.Unmarshal(resBody, &sir)
if err != nil {
return nil, errors.Wrap(err, "Error unmarshaling user provided service instances")
}
for _, instance := range sir.Resources {
instance.Entity.Guid = instance.Meta.Guid
instance.Entity.CreatedAt = instance.Meta.CreatedAt
instance.Entity.UpdatedAt = instance.Meta.UpdatedAt
instance.Entity.c = c
instances = append(instances, instance.Entity)
}
requestUrl = sir.NextUrl
if requestUrl == "" {
break
}
}
return instances, nil
}
func (c *Client) ListUserProvidedServiceInstances() ([]UserProvidedServiceInstance, error) {
return c.ListUserProvidedServiceInstancesByQuery(nil)
}
func (c *Client) GetUserProvidedServiceInstanceByGuid(guid string) (UserProvidedServiceInstance, error) {
var sir UserProvidedServiceInstanceResource
req := c.NewRequest("GET", "/v2/user_provided_service_instances/"+guid)
res, err := c.DoRequest(req)
if err != nil {
return UserProvidedServiceInstance{}, errors.Wrap(err, "Error requesting user provided service instance")
}
data, err := ioutil.ReadAll(res.Body)
if err != nil {
return UserProvidedServiceInstance{}, errors.Wrap(err, "Error reading user provided service instance response")
}
err = json.Unmarshal(data, &sir)
if err != nil {
return UserProvidedServiceInstance{}, errors.Wrap(err, "Error JSON parsing user provided service instance response")
}
sir.Entity.Guid = sir.Meta.Guid
sir.Entity.CreatedAt = sir.Meta.CreatedAt
sir.Entity.UpdatedAt = sir.Meta.UpdatedAt
sir.Entity.c = c
return sir.Entity, nil
}
func (c *Client) UserProvidedServiceInstanceByGuid(guid string) (UserProvidedServiceInstance, error) {
return c.GetUserProvidedServiceInstanceByGuid(guid)
}
func (c *Client) CreateUserProvidedServiceInstance(req UserProvidedServiceInstanceRequest) (*UserProvidedServiceInstance, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return nil, err
}
r := c.NewRequestWithBody("POST", "/v2/user_provided_service_instances", buf)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleUserProvidedServiceInstanceResp(resp)
}
func (c *Client) DeleteUserProvidedServiceInstance(guid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/user_provided_service_instances/%s", guid)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting user provided service instance %s, response code %d", guid, resp.StatusCode)
}
return nil
}
func (c *Client) UpdateUserProvidedServiceInstance(guid string, req UserProvidedServiceInstanceRequest) (*UserProvidedServiceInstance, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return nil, err
}
r := c.NewRequestWithBody("PUT", fmt.Sprintf("/v2/user_provided_service_instances/%s", guid), buf)
resp, err := c.DoRequest(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("CF API returned with status code %d", resp.StatusCode)
}
return c.handleUserProvidedServiceInstanceResp(resp)
}
func (c *Client) handleUserProvidedServiceInstanceResp(resp *http.Response) (*UserProvidedServiceInstance, error) {
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return nil, err
}
var upsResource UserProvidedServiceInstanceResource
err = json.Unmarshal(body, &upsResource)
if err != nil {
return nil, err
}
return c.mergeUserProvidedServiceInstanceResource(upsResource), nil
}
func (c *Client) mergeUserProvidedServiceInstanceResource(ups UserProvidedServiceInstanceResource) *UserProvidedServiceInstance {
ups.Entity.Guid = ups.Meta.Guid
ups.Entity.CreatedAt = ups.Meta.CreatedAt
ups.Entity.UpdatedAt = ups.Meta.UpdatedAt
ups.Entity.c = c
return &ups.Entity
}

View File

@ -0,0 +1,201 @@
package cfclient
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"github.com/pkg/errors"
)
type UserRequest struct {
Guid string `json:"guid"`
DefaultSpaceGuid string `json:"default_space_guid,omitempty"`
}
type Users []User
type User struct {
Guid string `json:"guid"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Admin bool `json:"admin"`
Active bool `json:"active"`
DefaultSpaceGUID string `json:"default_space_guid"`
Username string `json:"username"`
SpacesURL string `json:"spaces_url"`
OrgsURL string `json:"organizations_url"`
ManagedOrgsURL string `json:"managed_organizations_url"`
BillingManagedOrgsURL string `json:"billing_managed_organizations_url"`
AuditedOrgsURL string `json:"audited_organizations_url"`
ManagedSpacesURL string `json:"managed_spaces_url"`
AuditedSpacesURL string `json:"audited_spaces_url"`
c *Client
}
type UserResource struct {
Meta Meta `json:"metadata"`
Entity User `json:"entity"`
}
type UserResponse struct {
Count int `json:"total_results"`
Pages int `json:"total_pages"`
NextUrl string `json:"next_url"`
Resources []UserResource `json:"resources"`
}
// GetUserByGUID retrieves the user with the provided guid.
func (c *Client) GetUserByGUID(guid string) (User, error) {
var userRes UserResource
r := c.NewRequest("GET", "/v2/users/"+guid)
resp, err := c.DoRequest(r)
if err != nil {
return User{}, err
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return User{}, err
}
err = json.Unmarshal(body, &userRes)
if err != nil {
return User{}, err
}
return c.mergeUserResource(userRes), nil
}
func (c *Client) ListUsersByQuery(query url.Values) (Users, error) {
var users []User
requestUrl := "/v2/users?" + query.Encode()
for {
userResp, err := c.getUserResponse(requestUrl)
if err != nil {
return []User{}, err
}
for _, user := range userResp.Resources {
user.Entity.Guid = user.Meta.Guid
user.Entity.CreatedAt = user.Meta.CreatedAt
user.Entity.UpdatedAt = user.Meta.UpdatedAt
user.Entity.c = c
users = append(users, user.Entity)
}
requestUrl = userResp.NextUrl
if requestUrl == "" {
break
}
}
return users, nil
}
func (c *Client) ListUsers() (Users, error) {
return c.ListUsersByQuery(nil)
}
func (c *Client) ListUserSpaces(userGuid string) ([]Space, error) {
return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/spaces", userGuid))
}
func (c *Client) ListUserAuditedSpaces(userGuid string) ([]Space, error) {
return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/audited_spaces", userGuid))
}
func (c *Client) ListUserManagedSpaces(userGuid string) ([]Space, error) {
return c.fetchSpaces(fmt.Sprintf("/v2/users/%s/managed_spaces", userGuid))
}
func (c *Client) ListUserOrgs(userGuid string) ([]Org, error) {
return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/organizations", userGuid))
}
func (c *Client) ListUserManagedOrgs(userGuid string) ([]Org, error) {
return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/managed_organizations", userGuid))
}
func (c *Client) ListUserAuditedOrgs(userGuid string) ([]Org, error) {
return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/audited_organizations", userGuid))
}
func (c *Client) ListUserBillingManagedOrgs(userGuid string) ([]Org, error) {
return c.fetchOrgs(fmt.Sprintf("/v2/users/%s/billing_managed_organizations", userGuid))
}
func (c *Client) CreateUser(req UserRequest) (User, error) {
buf := bytes.NewBuffer(nil)
err := json.NewEncoder(buf).Encode(req)
if err != nil {
return User{}, err
}
r := c.NewRequestWithBody("POST", "/v2/users", buf)
resp, err := c.DoRequest(r)
if err != nil {
return User{}, err
}
if resp.StatusCode != http.StatusCreated {
return User{}, errors.Wrapf(err, "Error creating user, response code: %d", resp.StatusCode)
}
body, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return User{}, err
}
var userResource UserResource
err = json.Unmarshal(body, &userResource)
if err != nil {
return User{}, err
}
user := userResource.Entity
user.Guid = userResource.Meta.Guid
user.c = c
return user, nil
}
func (c *Client) DeleteUser(userGuid string) error {
resp, err := c.DoRequest(c.NewRequest("DELETE", fmt.Sprintf("/v2/users/%s", userGuid)))
if err != nil {
return err
}
if resp.StatusCode != http.StatusNoContent {
return errors.Wrapf(err, "Error deleting user %s, response code: %d", userGuid, resp.StatusCode)
}
return nil
}
func (u Users) GetUserByUsername(username string) User {
for _, user := range u {
if user.Username == username {
return user
}
}
return User{}
}
func (c *Client) getUserResponse(requestUrl string) (UserResponse, error) {
var userResp UserResponse
r := c.NewRequest("GET", requestUrl)
resp, err := c.DoRequest(r)
if err != nil {
return UserResponse{}, errors.Wrap(err, "Error requesting users")
}
resBody, err := ioutil.ReadAll(resp.Body)
defer resp.Body.Close()
if err != nil {
return UserResponse{}, errors.Wrap(err, "Error reading user request")
}
err = json.Unmarshal(resBody, &userResp)
if err != nil {
return UserResponse{}, errors.Wrap(err, "Error unmarshalling user")
}
return userResp, nil
}
func (c *Client) mergeUserResource(u UserResource) User {
u.Entity.Guid = u.Meta.Guid
u.Entity.CreatedAt = u.Meta.CreatedAt
u.Entity.UpdatedAt = u.Meta.UpdatedAt
u.Entity.c = c
return u.Entity
}

View File

@ -0,0 +1,17 @@
package cfclient
// Pagination is used by the V3 apis
type Pagination struct {
TotalResults int `json:"total_results"`
TotalPages int `json:"total_pages"`
First Link `json:"first"`
Last Link `json:"last"`
Next interface{} `json:"next"`
Previous interface{} `json:"previous"`
}
// Link is a HATEOAS-style link for v3 apis
type Link struct {
Href string `json:"href"`
Method string `json:"method,omitempty"`
}

38
vendor/github.com/hashicorp/go-hclog/context.go generated vendored Normal file
View File

@ -0,0 +1,38 @@
package hclog
import (
"context"
)
// WithContext inserts a logger into the context and is retrievable
// with FromContext. The optional args can be set with the same syntax as
// Logger.With to set fields on the inserted logger. This will not modify
// the logger argument in-place.
func WithContext(ctx context.Context, logger Logger, args ...interface{}) context.Context {
// While we could call logger.With even with zero args, we have this
// check to avoid unnecessary allocations around creating a copy of a
// logger.
if len(args) > 0 {
logger = logger.With(args...)
}
return context.WithValue(ctx, contextKey, logger)
}
// FromContext returns a logger from the context. This will return L()
// (the default logger) if no logger is found in the context. Therefore,
// this will never return a nil value.
func FromContext(ctx context.Context) Logger {
logger, _ := ctx.Value(contextKey).(Logger)
if logger == nil {
return L()
}
return logger
}
// Unexported new type so that our context key never collides with another.
type contextKeyType struct{}
// contextKey is the key used for the context to store the logger.
var contextKey = contextKeyType{}

View File

@ -22,7 +22,11 @@ var (
// to be used in more specific contexts.
func Default() Logger {
protect.Do(func() {
def = New(DefaultOptions)
// If SetDefault was used before Default() was called, we need to
// detect that here.
if def == nil {
def = New(DefaultOptions)
}
})
return def
@ -32,3 +36,13 @@ func Default() Logger {
func L() Logger {
return Default()
}
// SetDefault changes the logger to be returned by Default()and L()
// to the one given. This allows packages to use the default logger
// and have higher level packages change it to match the execution
// environment. It returns any old default if there is one.
func SetDefault(log Logger) Logger {
old := def
def = log
return old
}

View File

@ -21,6 +21,9 @@ import (
// contains millisecond precision
const TimeFormat = "2006-01-02T15:04:05.000Z0700"
// errJsonUnsupportedTypeMsg is included in log json entries, if an arg cannot be serialized to json
const errJsonUnsupportedTypeMsg = "logging contained values that don't serialize to json"
var (
_levelToBracket = map[Level]string{
Debug: "[DEBUG]",
@ -296,39 +299,7 @@ func (l *intLogger) renderSlice(v reflect.Value) string {
// JSON logging function
func (l *intLogger) logJSON(t time.Time, level Level, msg string, args ...interface{}) {
vals := map[string]interface{}{
"@message": msg,
"@timestamp": t.Format("2006-01-02T15:04:05.000000Z07:00"),
}
var levelStr string
switch level {
case Error:
levelStr = "error"
case Warn:
levelStr = "warn"
case Info:
levelStr = "info"
case Debug:
levelStr = "debug"
case Trace:
levelStr = "trace"
default:
levelStr = "all"
}
vals["@level"] = levelStr
if l.name != "" {
vals["@module"] = l.name
}
if l.caller {
if _, file, line, ok := runtime.Caller(3); ok {
vals["@caller"] = fmt.Sprintf("%s:%d", file, line)
}
}
vals := l.jsonMapEntry(t, level, msg)
args = append(l.implied, args...)
if args != nil && len(args) > 0 {
@ -369,10 +340,51 @@ func (l *intLogger) logJSON(t time.Time, level Level, msg string, args ...interf
err := json.NewEncoder(l.writer).Encode(vals)
if err != nil {
panic(err)
if _, ok := err.(*json.UnsupportedTypeError); ok {
plainVal := l.jsonMapEntry(t, level, msg)
plainVal["@warn"] = errJsonUnsupportedTypeMsg
json.NewEncoder(l.writer).Encode(plainVal)
}
}
}
func (l intLogger) jsonMapEntry(t time.Time, level Level, msg string) map[string]interface{} {
vals := map[string]interface{}{
"@message": msg,
"@timestamp": t.Format("2006-01-02T15:04:05.000000Z07:00"),
}
var levelStr string
switch level {
case Error:
levelStr = "error"
case Warn:
levelStr = "warn"
case Info:
levelStr = "info"
case Debug:
levelStr = "debug"
case Trace:
levelStr = "trace"
default:
levelStr = "all"
}
vals["@level"] = levelStr
if l.name != "" {
vals["@module"] = l.name
}
if l.caller {
if _, file, line, ok := runtime.Caller(4); ok {
vals["@caller"] = fmt.Sprintf("%s:%d", file, line)
}
}
return vals
}
// Emit the message and args at DEBUG level
func (l *intLogger) Debug(msg string, args ...interface{}) {
l.Log(Debug, msg, args...)
@ -507,5 +519,9 @@ func (l *intLogger) StandardLogger(opts *StandardLoggerOptions) *log.Logger {
}
func (l *intLogger) StandardWriter(opts *StandardLoggerOptions) io.Writer {
return &stdlogAdapter{l, opts.InferLevels}
return &stdlogAdapter{
log: l,
inferLevels: opts.InferLevels,
forceLevel: opts.ForceLevel,
}
}

View File

@ -143,6 +143,12 @@ type StandardLoggerOptions struct {
// This supports the strings like [ERROR], [ERR] [TRACE], [WARN], [INFO],
// [DEBUG] and strip it off before reapplying it.
InferLevels bool
// ForceLevel is used to force all output from the standard logger to be at
// the specified level. Similar to InferLevels, this will strip any level
// prefix contained in the logged string before applying the forced level.
// If set, this override InferLevels.
ForceLevel Level
}
// LoggerOptions can be used to configure a new logger.

View File

@ -11,6 +11,7 @@ import (
type stdlogAdapter struct {
log Logger
inferLevels bool
forceLevel Level
}
// Take the data, infer the levels if configured, and send it through
@ -18,7 +19,27 @@ type stdlogAdapter struct {
func (s *stdlogAdapter) Write(data []byte) (int, error) {
str := string(bytes.TrimRight(data, " \t\n"))
if s.inferLevels {
if s.forceLevel != NoLevel {
// Use pickLevel to strip log levels included in the line since we are
// forcing the level
_, str := s.pickLevel(str)
// Log at the forced level
switch s.forceLevel {
case Trace:
s.log.Trace(str)
case Debug:
s.log.Debug(str)
case Info:
s.log.Info(str)
case Warn:
s.log.Warn(str)
case Error:
s.log.Error(str)
default:
s.log.Info(str)
}
} else if s.inferLevels {
level, str := s.pickLevel(str)
switch level {
case Trace:

View File

@ -0,0 +1,22 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Jetbrains
.idea*
# Binaries
cmd/vault-plugin-auth-pcf/vault-plugin-auth-pcf
cmd/verify/verify
pkg*
bin*

View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@ -0,0 +1,57 @@
TOOL?=vault-plugin-auth-pcf
TEST?=$$(go list ./... | grep -v /vendor/ | grep -v teamcity)
VETARGS?=-asmdecl -atomic -bool -buildtags -copylocks -methods -nilfunc -printf -rangeloops -shift -structtags -unsafeptr
EXTERNAL_TOOLS=\
github.com/mitchellh/gox
BUILD_TAGS?=${TOOL}
GOFMT_FILES?=$$(find . -name '*.go' | grep -v vendor)
# bin generates the releaseable binaries for this plugin
bin: fmtcheck generate
@CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' sh -c "'$(CURDIR)/scripts/build.sh'"
default: dev
# dev creates binaries for testing Vault locally. These are put
# into ./bin/ as well as $GOPATH/bin.
dev: fmtcheck generate
@CGO_ENABLED=0 BUILD_TAGS='$(BUILD_TAGS)' VAULT_DEV_BUILD=1 sh -c "'$(CURDIR)/scripts/build.sh'"
# testshort runs the quick unit tests and vets the code
testshort: fmtcheck generate
CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -short -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=20m -parallel=4
# test runs the unit tests and vets the code
test: fmtcheck generate
CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test ./... -v -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -count=1 -timeout=20m -parallel=4
testcompile: fmtcheck generate
@for pkg in $(TEST) ; do \
go test -v -c -tags='$(BUILD_TAGS)' $$pkg -parallel=4 ; \
done
# generate runs `go generate` to build the dynamically generated
# source files.
generate:
go generate $(go list ./... | grep -v /vendor/)
# bootstrap the build by downloading additional tools
bootstrap:
@for tool in $(EXTERNAL_TOOLS) ; do \
echo "Installing/Updating $$tool" ; \
go get -u $$tool; \
done
fmtcheck:
@sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'"
fmt:
gofmt -w $(GOFMT_FILES)
proto:
protoc *.proto --go_out=plugins=grpc:.
tools:
go install ./...
.PHONY: bin default generate test vet bootstrap fmt fmtcheck

View File

@ -0,0 +1,483 @@
# vault-plugin-auth-pcf
This plugin leverages PCF's [App and Container Identity Assurance](https://content.pivotal.io/blog/new-in-pcf-2-1-app-container-identity-assurance-via-automatic-cert-rotation)
for authenticating to Vault.
## Known Risks
This authentication engine uses PCF's instance identity service to authenticate users to Vault. Because PCF
makes its CA certificate and **private key** available to certain users at any time, it's possible for someone
with access to them to self-issue identity certificates that meet the criteria for a Vault role, allowing
them to gain unintended access to Vault.
For this reason, we recommend that if you choose this auth method, you **carefully guard access to
the private key** for your instance identity CA certificate. In CredHub, it can be obtained through the
following call: `$ credhub get -n /cf/diego-instance-identity-root-ca`.
Take extra steps to limit access to that path in CredHub, whether it be through use of CredHub's ACL system,
or through carefully limiting the users who can access CredHub.
## Getting Started
### Obtaining Your Instance Identity CA Certificate
In most versions of PCF, instance identity is enabled out-of-the-box. Check by pulling your CA certificate,
which you'll need to configure this auth engine. There are undoubtedly multiple ways to do this, but this
is how we did it.
#### From CF Dev
```
$ bosh int --path /diego_instance_identity_ca ~/.cfdev/state/bosh/creds.yml
```
#### From CredHub
[Install and authenticate to the PCF command line tool](https://docs.pivotal.io/tiledev/2-2/pcf-command.html),
and [install jq](https://stedolan.github.io/jq/).
Get the credentials you'll use for CredHub:
```
$ pcf settings | jq '.products[0].director_credhub_client_credentials'
```
SSH into your Ops Manager VM:
```
ssh -i ops_mgr.pem ubuntu@$OPS_MGR_URL
```
Please note that the above `OPS_MGR_URL` shouldn't be prepended with `https://`.
Log in to Credhub using the credentials you obtained earlier:
```
$ credhub login --client-name=director_to_credhub --client-secret=CoJPkrsYi3c-Fx2QHEEDyaEEUuOfYMzw
```
6. Retrieve the CA information:
```
$ credhub get -n /cf/diego-instance-identity-root-ca
```
7. You'll receive a response like:
```
id: be2bd996-1d35-443b-b81c-90095024d5e7
name: /cf/diego-instance-identity-root-ca
type: certificate
value:
ca: |
-----BEGIN CERTIFICATE-----
MIIDNDCCAhygAwIBAgITPqTy1qvfHNEVuxsl9l1glY85OTANBgkqhkiG9w0BAQsF
ADAqMSgwJgYDVQQDEx9EaWVnbyBJbnN0YW5jZSBJZGVudGl0eSBSb290IENBMB4X
DTE5MDYwNjA5MTIwMVoXDTIyMDYwNTA5MTIwMVowKjEoMCYGA1UEAxMfRGllZ28g
SW5zdGFuY2UgSWRlbnRpdHkgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBALa8xGDYT/q3UzEKAsLDajhuHxPpIPFlCXwp6u8U5Qrf427Xof7n
rXRKzRu3g7E20U/OwzgBi3VZs8T29JGNWeA2k0HtX8oQ+Wc8Qngz9M8t1h9SZlx5
fGfxPt3x7xozaIGJ8p4HKQH1ZlirL7dzun7Y+7m6Ey8cMVsepqUs64r8+KpCbxKJ
rV04qtTNlr0LG3yOxSHlip+DDvUVL3jSFz/JDWxwCymiFBAh0QjG1LKp2FisURoX
GY+HJbf2StpK3i4dYnxQXQlMDpipozK7WFxv3gH4Q6YMZvlmIPidAF8FxfDIsYcq
TgQ5q0pr9mbu8oKbZ74vyZMqiy+r9vLhbu0CAwEAAaNTMFEwHQYDVR0OBBYEFAHf
pwqBhZ8/A6ZAvU+p5JPz/omjMB8GA1UdIwQYMBaAFAHfpwqBhZ8/A6ZAvU+p5JPz
/omjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADuDJev+6bOC
v7t9SS4Nd/zeREuF9IKsHDHrYUZBIO1aBQbOO1iDtL4VA3LBEx6fOgN5fbxroUsz
X9/6PtxLe+5U8i5MOztK+OxxPrtDfnblXVb6IW4EKhTnWesS7R2WnOWtzqRQXKFU
voBn3QckLV1o9eqzYIE/aob4z0GaVanA9PSzzbVPsX79RCD1B7NmV0cKEQ7IrCrh
L7ElDV/GlNrtVdHjY0mwz9iu+0YJvxvcHDTERi106b28KXzJz+P5/hyg2wqRXzdI
faXAjW0kuq5nxyJUALwxD/8pz77uNt4w6WfJoSDM6XrAIhh15K3tZg9EzBmAZ/5D
jK0RcmCyaXw=
-----END CERTIFICATE-----
certificate: |
-----BEGIN CERTIFICATE-----
MIIDNDCCAhygAwIBAgITPqTy1qvfHNEVuxsl9l1glY85OTANBgkqhkiG9w0BAQsF
ADAqMSgwJgYDVQQDEx9EaWVnbyBJbnN0YW5jZSBJZGVudGl0eSBSb290IENBMB4X
DTE5MDYwNjA5MTIwMVoXDTIyMDYwNTA5MTIwMVowKjEoMCYGA1UEAxMfRGllZ28g
SW5zdGFuY2UgSWRlbnRpdHkgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBALa8xGDYT/q3UzEKAsLDajhuHxPpIPFlCXwp6u8U5Qrf427Xof7n
rXRKzRu3g7E20U/OwzgBi3VZs8T29JGNWeA2k0HtX8oQ+Wc8Qngz9M8t1h9SZlx5
fGfxPt3x7xozaIGJ8p4HKQH1ZlirL7dzun7Y+7m6Ey8cMVsepqUs64r8+KpCbxKJ
rV04qtTNlr0LG3yOxSHlip+DDvUVL3jSFz/JDWxwCymiFBAh0QjG1LKp2FisURoX
GY+HJbf2StpK3i4dYnxQXQlMDpipozK7WFxv3gH4Q6YMZvlmIPidAF8FxfDIsYcq
TgQ5q0pr9mbu8oKbZ74vyZMqiy+r9vLhbu0CAwEAAaNTMFEwHQYDVR0OBBYEFAHf
pwqBhZ8/A6ZAvU+p5JPz/omjMB8GA1UdIwQYMBaAFAHfpwqBhZ8/A6ZAvU+p5JPz
/omjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADuDJev+6bOC
v7t9SS4Nd/zeREuF9IKsHDHrYUZBIO1aBQbOO1iDtL4VA3LBEx6fOgN5fbxroUsz
X9/6PtxLe+5U8i5MOztK+OxxPrtDfnblXVb6IW4EKhTnWesS7R2WnOWtzqRQXKFU
voBn3QckLV1o9eqzYIE/aob4z0GaVanA9PSzzbVPsX79RCD1B7NmV0cKEQ7IrCrh
L7ElDV/GlNrtVdHjY0mwz9iu+0YJvxvcHDTERi106b28KXzJz+P5/hyg2wqRXzdI
faXAjW0kuq5nxyJUALwxD/8pz77uNt4w6WfJoSDM6XrAIhh15K3tZg9EzBmAZ/5D
jK0RcmCyaXw=
-----END CERTIFICATE-----
private_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAtrzEYNhP+rdTMQoCwsNqOG4fE+kg8WUJfCnq7xTlCt/jbteh
/uetdErNG7eDsTbRT87DOAGLdVmzxPb0kY1Z4DaTQe1fyhD5ZzxCeDP0zy3WH1Jm
XHl8Z/E+3fHvGjNogYnyngcpAfVmWKsvt3O6ftj7uboTLxwxWx6mpSzrivz4qkJv
EomtXTiq1M2WvQsbfI7FIeWKn4MO9RUveNIXP8kNbHALKaIUECHRCMbUsqnYWKxR
GhcZj4clt/ZK2kreLh1ifFBdCUwOmKmjMrtYXG/eAfhDpgxm+WYg+J0AXwXF8Mix
hypOBDmrSmv2Zu7ygptnvi/JkyqLL6v28uFu7QIDAQABAoIBACGIPhjvWK3PGirz
hVIr/b/hJT7IFs11Fup73qqEkQsPznI2i3l1FfUzDLQ7VqUcRAh7DoOmdOrRzRUl
o/dZktZ77UW5w0wXFU0GV8Qq9I9X/+S7gCEUAeoo8LpVfOS37kNnBuhMtA+x8lfv
AdCOIfjI5FhOdtq8N6pa04WX2pkkRzQkIpneRcLPqq1WwKZK1o8zxCnbP+4SI8yo
dTB2ldDY+1vusMYoFch2IsPDMCxVUpAYOoO0jvyOM4cqm0m7P+Tb6Qy19GG50ZfC
PlEK76YTOurdirGWdnGC+JEf4smrGgIvJGKEb55/qbqPow/o6Bf4XVXnOHaboW/W
Mu4cGFkCgYEA2JaQxFF51yuz4W7/VFZCCix3woPH12fZ3fz7aVMUfn4WQwMbNLVa
7G4gdRclReOs5FLM7jPqiTxDckXnoWRc5Ff9Xn4c2ioXrqzXr5V4qJ4GAWxAx0uM
w1u5ZpVL2HO12pat74MnYw7EJ65oznQNFSC1FAGn5BJ9f5HFk4X3ngMCgYEA1/1Q
XmAk1XJUQ0RP0EehwNipZQPhodGmrqHBGED+N/8+eRMo4shi//EcPXZ222j9kqAE
inPA9qaDxhBjgMt+JBFkj/bmTO/Yz8XusBBa5YlN9Ev30zlO+dRlM41/piluPTzf
vNQuzyNIzl2Gzd71R1TcuFWIDxn8BR0/cBA/5E8CgYAT7m8uEc1jlrr8AOnwSevT
4dm3hccLNJxhCFnejG2zYkkMK6oCRLo0TcIg5Ftivhv3+wKu3Qo1TN1sE7DIMmM2
BD7lxjdDgGIjifZjSx8KbVhiIyMm8/XlOHisTwrmxWcz0W/6PZiPThmRCUTN0vIt
QpBHYgugOm9gIPsMo2RxHwKBgQDOUDjZvUrR3GCi1HjMwe+/bvX3+MopMULfYsE4
srRittxs+KFAZxsx0ZUhHKySDurQiSttOP6kXBBZPERfvYFjYH3HipcX/K8EYNQL
t8OrqAkfhwVV7VMEDx8QLGQ3SzHzKteo3qFL2S9teCcRNZzjoysmpQTPMAnstLBp
EgyFvwKBgQDObNn/Kmfwi6TuGhIjLtBuUEhp5n4EUtysTUZs/15h02MWOfI8CCvm
xWb6/vZrVggxGlZgZtKy9+COPVpEMFaVdwq9uq4lW77sSBwGIwfzHd1CIjce6mSg
P5+wO3aTgvr4n8D5NyWcnYPJKRQzqWHHnfk+9TQA1l0g3/yQXfCx2A==
-----END RSA PRIVATE KEY-----
version_created_at: "2019-06-06T09:12:01Z"
```
From that response, copy the first certificate (under `ca: |`) and place
it into its own separate file using a plain text editor like [Sublime](https://www.sublimetext.com/).
The following instructions assume you name the file `ca.crt`.
Remove any tabs before each line, and any trailing space or lines. When
complete, your CA certificate should look like this:
```
$ cat ca.crt
-----BEGIN CERTIFICATE-----
MIIDNDCCAhygAwIBAgITPqTy1qvfHNEVuxsl9l1glY85OTANBgkqhkiG9w0BAQsF
ADAqMSgwJgYDVQQDEx9EaWVnbyBJbnN0YW5jZSBJZGVudGl0eSBSb290IENBMB4X
DTE5MDYwNjA5MTIwMVoXDTIyMDYwNTA5MTIwMVowKjEoMCYGA1UEAxMfRGllZ28g
SW5zdGFuY2UgSWRlbnRpdHkgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP
ADCCAQoCggEBALa8xGDYT/q3UzEKAsLDajhuHxPpIPFlCXwp6u8U5Qrf427Xof7n
rXRKzRu3g7E20U/OwzgBi3VZs8T29JGNWeA2k0HtX8oQ+Wc8Qngz9M8t1h9SZlx5
fGfxPt3x7xozaIGJ8p4HKQH1ZlirL7dzun7Y+7m6Ey8cMVsepqUs64r8+KpCbxKJ
rV04qtTNlr0LG3yOxSHlip+DDvUVL3jSFz/JDWxwCymiFBAh0QjG1LKp2FisURoX
GY+HJbf2StpK3i4dYnxQXQlMDpipozK7WFxv3gH4Q6YMZvlmIPidAF8FxfDIsYcq
TgQ5q0pr9mbu8oKbZ74vyZMqiy+r9vLhbu0CAwEAAaNTMFEwHQYDVR0OBBYEFAHf
pwqBhZ8/A6ZAvU+p5JPz/omjMB8GA1UdIwQYMBaAFAHfpwqBhZ8/A6ZAvU+p5JPz
/omjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADuDJev+6bOC
v7t9SS4Nd/zeREuF9IKsHDHrYUZBIO1aBQbOO1iDtL4VA3LBEx6fOgN5fbxroUsz
X9/6PtxLe+5U8i5MOztK+OxxPrtDfnblXVb6IW4EKhTnWesS7R2WnOWtzqRQXKFU
voBn3QckLV1o9eqzYIE/aob4z0GaVanA9PSzzbVPsX79RCD1B7NmV0cKEQ7IrCrh
L7ElDV/GlNrtVdHjY0mwz9iu+0YJvxvcHDTERi106b28KXzJz+P5/hyg2wqRXzdI
faXAjW0kuq5nxyJUALwxD/8pz77uNt4w6WfJoSDM6XrAIhh15K3tZg9EzBmAZ/5D
jK0RcmCyaXw=
-----END CERTIFICATE-----
```
Verify that this certificate can be properly parsed like so:
```
$ openssl x509 -in ca.crt -text -noout
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
3e:a4:f2:d6:ab:df:1c:d1:15:bb:1b:25:f6:5d:60:95:8f:39:39
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Diego Instance Identity Root CA
Validity
Not Before: Jun 6 09:12:01 2019 GMT
Not After : Jun 5 09:12:01 2022 GMT
Subject: CN=Diego Instance Identity Root CA
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:b6:bc:c4:60:d8:4f:fa:b7:53:31:0a:02:c2:c3:
6a:38:6e:1f:13:e9:20:f1:65:09:7c:29:ea:ef:14:
e5:0a:df:e3:6e:d7:a1:fe:e7:ad:74:4a:cd:1b:b7:
83:b1:36:d1:4f:ce:c3:38:01:8b:75:59:b3:c4:f6:
f4:91:8d:59:e0:36:93:41:ed:5f:ca:10:f9:67:3c:
42:78:33:f4:cf:2d:d6:1f:52:66:5c:79:7c:67:f1:
3e:dd:f1:ef:1a:33:68:81:89:f2:9e:07:29:01:f5:
66:58:ab:2f:b7:73:ba:7e:d8:fb:b9:ba:13:2f:1c:
31:5b:1e:a6:a5:2c:eb:8a:fc:f8:aa:42:6f:12:89:
ad:5d:38:aa:d4:cd:96:bd:0b:1b:7c:8e:c5:21:e5:
8a:9f:83:0e:f5:15:2f:78:d2:17:3f:c9:0d:6c:70:
0b:29:a2:14:10:21:d1:08:c6:d4:b2:a9:d8:58:ac:
51:1a:17:19:8f:87:25:b7:f6:4a:da:4a:de:2e:1d:
62:7c:50:5d:09:4c:0e:98:a9:a3:32:bb:58:5c:6f:
de:01:f8:43:a6:0c:66:f9:66:20:f8:9d:00:5f:05:
c5:f0:c8:b1:87:2a:4e:04:39:ab:4a:6b:f6:66:ee:
f2:82:9b:67:be:2f:c9:93:2a:8b:2f:ab:f6:f2:e1:
6e:ed
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
01:DF:A7:0A:81:85:9F:3F:03:A6:40:BD:4F:A9:E4:93:F3:FE:89:A3
X509v3 Authority Key Identifier:
keyid:01:DF:A7:0A:81:85:9F:3F:03:A6:40:BD:4F:A9:E4:93:F3:FE:89:A3
X509v3 Basic Constraints: critical
CA:TRUE
Signature Algorithm: sha256WithRSAEncryption
3b:83:25:eb:fe:e9:b3:82:bf:bb:7d:49:2e:0d:77:fc:de:44:
4b:85:f4:82:ac:1c:31:eb:61:46:41:20:ed:5a:05:06:ce:3b:
58:83:b4:be:15:03:72:c1:13:1e:9f:3a:03:79:7d:bc:6b:a1:
4b:33:5f:df:fa:3e:dc:4b:7b:ee:54:f2:2e:4c:3b:3b:4a:f8:
ec:71:3e:bb:43:7e:76:e5:5d:56:fa:21:6e:04:2a:14:e7:59:
eb:12:ed:1d:96:9c:e5:ad:ce:a4:50:5c:a1:54:be:80:67:dd:
07:24:2d:5d:68:f5:ea:b3:60:81:3f:6a:86:f8:cf:41:9a:55:
a9:c0:f4:f4:b3:cd:b5:4f:b1:7e:fd:44:20:f5:07:b3:66:57:
47:0a:11:0e:c8:ac:2a:e1:2f:b1:25:0d:5f:c6:94:da:ed:55:
d1:e3:63:49:b0:cf:d8:ae:fb:46:09:bf:1b:dc:1c:34:c4:46:
2d:74:e9:bd:bc:29:7c:c9:cf:e3:f9:fe:1c:a0:db:0a:91:5f:
37:48:7d:a5:c0:8d:6d:24:ba:ae:67:c7:22:54:00:bc:31:0f:
ff:29:cf:be:ee:36:de:30:e9:67:c9:a1:20:cc:e9:7a:c0:22:
18:75:e4:ad:ed:66:0f:44:cc:19:80:67:fe:43:8c:ad:11:72:
60:b2:69:7c
```
Congratulations! You have obtained the CA certificate you'll use for configuring
this auth engine.
### Obtaining Your API Credentials
From the directory where you added `metadata` in the previous step to authenticate to the pcf command-line
tool, run the following commands:
```
$ pcf target
$ cf api
```
The api endpoint given will be used for configuring this Vault auth method.
This plugin was tested with Org Manager level permissions, but lower level permissions may be usable.
```
$ cf create-user vault pa55word
$ cf orgs
$ cf org-users my-example-org
$ cf set-org-role Alice my-example-org OrgManager
```
Since the PCF API tends to use a self-signed certificate, you'll also need to configure
Vault to trust that certificate. You can obtain its API certificate via:
```
openssl s_client -showcerts -servername domain.com -connect domain.com:443
```
You'll see a certificate outputted as part of the response, which should be broken
out into a separate well-formatted file like the `ca.crt` above, and used for the
`pcf_api_trusted_certificates` field.
## Downloading the Plugin
- `$ git clone git@github.com:hashicorp/vault-plugin-auth-pcf.git`
- `$ cd vault-plugin-auth-pcf`
- `$ PCF_HOME=$(pwd)`
## Sample Usage
Please note that this example uses `generate-signature`, a tool installed through `$ make tools`.
First, enable the PCF auth engine.
```
$ vault auth enable pcf
```
Next, configure the plugin. In the `config` call below, `certificates` is intended to be the instance
identity CA certificate you pulled above.
In the CF Dev environment the default API address is `https://api.dev.cfdev.sh`. The default username and password
are `admin`, `admin`. In a production environment, these attributes will vary.
```
$ vault write auth/pcf/config \
certificates=@ca.crt \
pcf_api_addr=https://api.dev.cfdev.sh \
pcf_username=admin \
pcf_password=admin \
pcf_api_trusted_certificates=@pcfapi.crt
```
Then, add a role that will be used to grant specific Vault policies to those logging in with it. When a constraint like
`bound_application_ids` is added, then the application ID on the cert used for logging in _must_ be one of the role's
application IDs. However, if `bound_application_ids` is omitted, then _any_ application ID will match. We recommend
configuring as many bound parameters as possible.
Also, by default, the IP address on the certificate presented at login must match that of the caller. However, if
your callers tend to be proxied, this may not work for you. If that's the case, set `disable_ip_matching` to true.
```
$ vault write auth/pcf/roles/test-role \
bound_application_ids=2d3e834a-3a25-4591-974c-fa5626d5d0a1 \
bound_space_ids=3d2eba6b-ef19-44d5-91dd-1975b0db5cc9 \
bound_organization_ids=34a878d0-c2f9-4521-ba73-a9f664e82c7bf \
policies=foo-policies
```
Logging in is intended to be performed using your `CF_INSTANCE_CERT` and `CF_INSTANCE_KEY`. This is an example of how
it can be done.
```
$ export CF_INSTANCE_CERT=$PCF_HOME/testdata/fake-certificates/instance.crt
$ export CF_INSTANCE_KEY=$PCF_HOME/testdata/fake-certificates/instance.key
$ vault login -method=pcf role=test-role
```
### Updating the CA Certificate
In PCF, most CA certificates expire after 4 years. However, it's possible to configure your own CA certificate for the
instance identity service, and its expiration date could vary. Either way, sometimes CA certificates expire and it may
be necessary to have multiple configured so the beginning date of once commences when another expires.
To configure multiple certificates, simply update the config to include the current one and future one.
```
$ CURRENT=$(cat /path/to/current-ca.crt)
$ FUTURE=$(cat /path/to/future-ca.crt)
$ vault write auth/vault-plugin-auth-pcf/config certificates="$CURRENT,$FUTURE"
```
All other configured values will remain untouched; however, the previous value for `certificates` will be overwritten
with the new one you've provided.
Providing a future CA certificate before the current one expires can protect you from having a downtime while the service
is switching over from the old to the new. If a client certificate was issued by _any_ CA certificate you've configured,
login will succeed.
## Troubleshooting
### Obtaining a Certificate Error from the PCF API
When configuring this plugin, you may encounter an error like:
```
Error writing data to auth/pcf/config: Error making API request.
URL: PUT http://127.0.0.1:8200/v1/auth/pcf/config
Code: 500. Errors:
* 1 error occurred:
* unable to establish an initial connection to the PCF API: Could not get api /v2/info: Get https://api.sys.lagunaniguel.cf-app.com/v2/info: x509: certificate signed by unknown authority
```
To resolve this error, review instructions above regarding setting the `pcf_api_trusted_certificates` field.
### verify-certs
This tool, installed by `make tools`, is for verifying that your CA certificate, client certificate, and client
key are all properly related to each other and will pass verification if used by this auth engine. If you're
debugging authentication problems that may be related to your certificates, it's a fantastic tool to use.
```
verify-certs -ca-cert=local/path/to/ca.crt -instance-cert=local/path/to/instance.crt -instance-key=local/path/to/instance.key
```
The `ca-cert` should be the cert that was used to issue the given client certificate.
The `instance-cert` given should be the value for the `CF_INSTANCE_CERT` variable in the PCF environment you're
using, and the `instance-key` should be the value for the `CF_INSTANCE_KEY`.
The tool does take the _local path to_ these certificates, so you'll need to gather them and place them on your
local machine to verify they all will work together.
### generate-signature
This tool, installed by `make tools`, is for generating a valid signature to be used for signing into Vault via PCF.
It can be used as a standalone tool for generating a signature like so:
```
export CF_INSTANCE_CERT=path/to/instance.crt
export CF_INSTANCE_KEY=path/to/instance.key
export SIGNING_TIME=$(date -u)
export ROLE='test-role'
generate-signature
```
It can also be used for signing into Vault like so:
```
export CF_INSTANCE_CERT=path/to/instance.crt
export CF_INSTANCE_KEY=path/to/instance.key
export SIGNING_TIME=$(date -u)
export ROLE='test-role'
vault write auth/vault-plugin-auth-pcf/login \
role=$ROLE \
certificate=$CF_INSTANCE_CERT \
signing-time=SIGNING_TIME \
signature=$(generate-signature)
```
If the tool is being run in a PCF environment already containing the `CF_INSTANCE_CERT` and `CF_INSTANCE_KEY`, those
variables obviously won't need to be manually set before the tool is used and can just be pulled as they are.
## Developing
### mock-pcf-server
This tool, installed by `make tools`, is for use in development. It lets you run a mocked PCF server for use in local
testing, with output that can be used as the `pcf_api_addr`, `pcf_username`, and `pcf_password` in your config.
Example use:
```
$ mock-pcf-server
running at http://127.0.0.1:33671
username is username
password is password
```
Simply hit CTRL+C to stop the test server.
### Implementing the Signature Algorithm in Other Languages
The signing algorithm used by this plugin is viewable in `signatures/version1.go`. There is also a test
called `TestSignature` in the same package that outputs a viewable signing string, hash of it, and
resulting signature. The signature will be different every time the test is run because some
of the input to the final signature includes cryptographically random material. This means that no matter
what you do, your final signature won't match any signatures shown; the important thing, however, is that
it can be verified as having been signed by the private key that's associated with the given client
certificate.
To develop your own version of the signing algorithm in a different language, we recommend you duplicate
the inputs to `TestSignature`, duplicate its signing string and hash, and duplicate the signing algorithm used.
### Quick Start
```
# After cloning the repo, generate fake certs, a test binary, and install the tools.
make test
make dev
make tools
# In one shell window, run Vault with the plugin available in the catalog.
vault server -dev -dev-root-token-id=root -dev-plugin-dir=$PCF_HOME/bin -log-level=debug
# In another shell window, run a mock of the PCF API so the plugin's client calls won't fail.
mock-pcf-server
# In another shell window, execute the following commands to exercise each endpoint.
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=root
export MOCK_PCF_SERVER_ADDR='something' # ex. http://127.0.0.1:32937
vault auth enable vault-plugin-auth-pcf
vault write auth/vault-plugin-auth-pcf/config \
certificates=@$PCF_HOME/testdata/fake-certificates/ca.crt \
pcf_api_addr=$MOCK_PCF_SERVER_ADDR \
pcf_username=username \
pcf_password=password
vault write auth/vault-plugin-auth-pcf/roles/test-role \
bound_application_ids=2d3e834a-3a25-4591-974c-fa5626d5d0a1 \
bound_space_ids=3d2eba6b-ef19-44d5-91dd-1975b0db5cc9 \
bound_organization_ids=34a878d0-c2f9-4521-ba73-a9f664e82c7bf \
bound_instance_ids=1bf2e7f6-2d1d-41ec-501c-c70 \
policies=foo,policies \
disable_ip_matching=true \
ttl=86400s \
max_ttl=86400s \
period=86400s
export CF_INSTANCE_CERT=$PCF_HOME/testdata/fake-certificates/instance.crt
export CF_INSTANCE_KEY=$PCF_HOME/testdata/fake-certificates/instance.key
export SIGNING_TIME=$(date -u)
export ROLE='test-role'
vault write auth/vault-plugin-auth-pcf/login \
role=$ROLE \
certificate=@$CF_INSTANCE_CERT \
signing_time="$SIGNING_TIME" \
signature=$(generate-signature)
vault token renew <token>
CURRENT=$(cat $PCF_HOME/testdata/fake-certificates/ca.crt)
FUTURE=$(cat $PCF_HOME/testdata/fake-certificates/ca.crt)
vault write auth/vault-plugin-auth-pcf/config certificates="$CURRENT,$FUTURE"
```

View File

@ -0,0 +1,49 @@
package pcf
import (
"context"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
const (
// These env vars are used frequently to pull the client certificate and private key
// from PCF containers; thus are placed here for ease of discovery and use from
// outside packages.
EnvVarInstanceCertificate = "CF_INSTANCE_CERT"
EnvVarInstanceKey = "CF_INSTANCE_KEY"
)
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := &backend{}
b.Backend = &framework.Backend{
AuthRenew: b.pathLoginRenew,
Help: backendHelp,
PathsSpecial: &logical.Paths{
SealWrapStorage: []string{"config"},
Unauthenticated: []string{"login"},
},
Paths: []*framework.Path{
b.pathConfig(),
b.pathListRoles(),
b.pathRoles(),
b.pathLogin(),
},
BackendType: logical.TypeCredential,
}
if err := b.Setup(ctx, conf); err != nil {
return nil, err
}
return b, nil
}
type backend struct {
*framework.Backend
}
const backendHelp = `
The PCF auth backend supports logging in using PCF's identity service.
Once a CA certificate is configured, and Vault is configured to consume
PCF's API, PCF's instance identity credentials can be used to authenticate.'
`

View File

@ -0,0 +1,115 @@
package pcf
import (
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
"github.com/hashicorp/vault-plugin-auth-pcf/signatures"
"github.com/hashicorp/vault/api"
)
type CLIHandler struct{}
func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) {
mount, ok := m["mount"]
if !ok {
mount = "pcf"
}
role := m["role"]
if role == "" {
return nil, errors.New(`"role" is required`)
}
pathToInstanceCert := m["cf_instance_cert"]
if pathToInstanceCert == "" {
pathToInstanceCert = os.Getenv(EnvVarInstanceCertificate)
}
if pathToInstanceCert == "" {
return nil, errors.New(`"cf_instance_cert" is required`)
}
pathToInstanceKey := m["cf_instance_key"]
if pathToInstanceKey == "" {
pathToInstanceKey = os.Getenv(EnvVarInstanceKey)
}
if pathToInstanceKey == "" {
return nil, errors.New(`"cf_instance_key" is required`)
}
certBytes, err := ioutil.ReadFile(pathToInstanceCert)
if err != nil {
return nil, err
}
cfInstanceCertContents := string(certBytes)
signingTime := time.Now().UTC()
signatureData := &signatures.SignatureData{
SigningTime: signingTime,
Role: role,
CFInstanceCertContents: cfInstanceCertContents,
}
signature, err := signatures.Sign(pathToInstanceKey, signatureData)
if err != nil {
return nil, err
}
loginData := map[string]interface{}{
"role": role,
"cf_instance_cert": cfInstanceCertContents,
"signing_time": signingTime.Format(signatures.TimeFormat),
"signature": signature,
}
path := fmt.Sprintf("auth/%s/login", mount)
secret, err := c.Logical().Write(path, loginData)
if err != nil {
return nil, err
}
if secret == nil {
return nil, errors.New("empty response from credential provider")
}
return secret, nil
}
func (h *CLIHandler) Help() string {
help := `
Usage: vault login -method=pcf [CONFIG K=V...]
The PCF auth method allows users to authenticate using PCF's instance identity service.
The PCF credentials may be specified explicitly via the command line:
$ vault login -method=pcf role=...
This will automatically pull from the CF_INSTANCE_CERT and CF_INSTANCE_KEY values
in your local environment. If they're not available or you wish to override them,
they may also be supplied explicitly:
$ vault login -method=pcf role=... cf_instance_cert=... cf_instance_key=...
Configuration:
cf_instance_cert=<string>
Explicit value to use for the path to the PCF instance certificate.
cf_instance_key=<string>
Explicit value to use for the path to the PCF instance key.
mount=<string>
Path where the PCF credential method is mounted. This is usually provided
via the -path flag in the "vault login" command, but it can be specified
here as well. If specified here, it takes precedence over the value for
-path. The default value is "pcf".
role=<string>
Name of the role to request a token against
`
return strings.TrimSpace(help)
}

View File

@ -0,0 +1,15 @@
module github.com/hashicorp/vault-plugin-auth-pcf
go 1.12
require (
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/hashicorp/go-cleanhttp v0.5.1
github.com/hashicorp/go-hclog v0.9.2
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/go-sockaddr v1.0.2
github.com/hashicorp/go-uuid v1.0.1
github.com/hashicorp/vault/api v1.0.2
github.com/hashicorp/vault/sdk v0.1.11
github.com/pkg/errors v0.8.1
)

View File

@ -0,0 +1,168 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f h1:UrKzEwTgeiff9vxdrfdqxibzpWjxLnuXDI5m6z3GJAk=
code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f/go.mod h1:sk5LnIjB/nIEU7yP5sDQExVm62wu0pBh3yrElngUisI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381 h1:rdRS5BT13Iae9ssvcslol66gfOOXjaLYwqerEn/cl9s=
github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381/go.mod h1:e5+USP2j8Le2M0Jo3qKPFnNhuo1wueU4nWHCXBOfQ14=
github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0/go.mod h1:4Zcjuz89kmFXt9morQgcfYZAYZ5n8WHjt81YYWIwtTM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-plugin v1.0.0 h1:/gQ1sNR8/LHpoxKRQq4PmLBuacfZb4tC93e9B30o/7c=
github.com/hashicorp/go-plugin v1.0.0/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.0 h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.1.0 h1:bPIoEKD27tNdebFGGxxYwcL4nepeY4j1QP23PFRGzg0=
github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.0.2 h1:/V9fULvLwt58vme/6Rkt/p/GtlresQv+Z9E6dgdANhs=
github.com/hashicorp/vault/api v1.0.2/go.mod h1:AV/+M5VPDpB90arloVX0rVDUIHkONiwz5Uza9HRtpUE=
github.com/hashicorp/vault/sdk v0.1.8/go.mod h1:tHZfc6St71twLizWNHvnnbiGFo1aq0eD2jGPLtP8kAU=
github.com/hashicorp/vault/sdk v0.1.11 h1:15dSaIT8p1Yq4Ac5OnlRGBdI5Ml/cqS84ObdM23kcA0=
github.com/hashicorp/vault/sdk v0.1.11/go.mod h1:XF2Bod+ahPWGARnyFq5LfkOZwWwvveR5ptYwJLqK0ZI=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/martini-contrib/render v0.0.0-20150707142108-ec18f8345a11/go.mod h1:Ah2dBMoxZEqk118as2T4u4fjfXarE0pPnMJaArZQZsI=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/oxtoacart/bpool v0.0.0-20150712133111-4e1c5567d7c2/go.mod h1:L3UMQOThbttwfYRNFOWLLVXMhk5Lkio4GGOtw5UrxS0=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pierrec/lz4 v2.0.5+incompatible h1:2xWsjqPFWcplujydGg4WmhC/6fZqK42wMM8aXeqhl0I=
github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/smartystreets/assertions v0.0.0-20180725160413-e900ae048470/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1 h1:VeAkjQVzKLmu+JnFcK96TPbkuaTIqwGGAzQ9hgwPjVg=
golang.org/x/oauth2 v0.0.0-20190130055435-99b60b757ec1/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190129075346-302c3dd5f1cc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db h1:6/JqlYfC1CCaLnGceQTI+sDGhC9UBSPAsBqI0Gun6kU=
golang.org/x/text v0.3.1-0.20181227161524-e6919f6577db/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo=
google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -0,0 +1,29 @@
package models
import "time"
// Configuration is the config as it's reflected in Vault's storage system.
type Configuration struct {
// IdentityCACertificates are the CA certificates that should be used for verifying client certificates.
IdentityCACertificates []string `json:"identity_ca_certificates"`
// IdentityCACertificates that, if presented by the PCF API, should be trusted.
PCFAPICertificates []string `json:"pcf_api_trusted_certificates"`
// PCFAPIAddr is the address of PCF's API, ex: "https://api.dev.cfdev.sh" or "http://127.0.0.1:33671"
PCFAPIAddr string `json:"pcf_api_addr"`
// The username for the PCF API.
PCFUsername string `json:"pcf_username"`
// The password for the PCF API.
PCFPassword string `json:"pcf_password"`
// The maximum seconds old a login request's signing time can be.
// This is configurable because in some test environments we found as much as 2 hours of clock drift.
LoginMaxSecOld time.Duration `json:"login_max_seconds_old"`
// The maximum seconds ahead a login request's signing time can be.
// This is configurable because in some test environments we found as much as 2 hours of clock drift.
LoginMaxSecAhead time.Duration `json:"login_max_seconds_ahead"`
}

View File

@ -0,0 +1,98 @@
package models
import (
"crypto/x509"
"errors"
"fmt"
"net"
"strings"
)
// NewPCFCertificateFromx509 converts a x509 certificate to a valid, well-formed PCF certificate,
// erroring if this isn't possible.
func NewPCFCertificateFromx509(certificate *x509.Certificate) (*PCFCertificate, error) {
if len(certificate.IPAddresses) != 1 {
return nil, fmt.Errorf("valid PCF certs have one IP address, but this has %s", certificate.IPAddresses)
}
pcfCert := &PCFCertificate{
InstanceID: certificate.Subject.CommonName,
IPAddress: certificate.IPAddresses[0],
}
spaces := 0
orgs := 0
apps := 0
for _, ou := range certificate.Subject.OrganizationalUnit {
if strings.HasPrefix(ou, "space:") {
pcfCert.SpaceID = strings.Split(ou, "space:")[1]
spaces++
continue
}
if strings.HasPrefix(ou, "organization:") {
pcfCert.OrgID = strings.Split(ou, "organization:")[1]
orgs++
continue
}
if strings.HasPrefix(ou, "app:") {
pcfCert.AppID = strings.Split(ou, "app:")[1]
apps++
continue
}
}
if spaces > 1 {
return nil, fmt.Errorf("expected 1 space but received %d", spaces)
}
if orgs > 1 {
return nil, fmt.Errorf("expected 1 org but received %d", orgs)
}
if apps > 1 {
return nil, fmt.Errorf("expected 1 app but received %d", apps)
}
if err := pcfCert.validate(); err != nil {
return nil, err
}
return pcfCert, nil
}
// NewPCFCertificateFromx509 converts the given fields to a valid, well-formed PCF certificate,
// erroring if this isn't possible.
func NewPCFCertificate(instanceID, orgID, spaceID, appID, ipAddress string) (*PCFCertificate, error) {
pcfCert := &PCFCertificate{
InstanceID: instanceID,
OrgID: orgID,
SpaceID: spaceID,
AppID: appID,
IPAddress: net.ParseIP(ipAddress),
}
if err := pcfCert.validate(); err != nil {
return nil, err
}
return pcfCert, nil
}
// PCFCertificate isn't intended to be instantiated directly; but rather through one of the New
// methods, which contain logic validating that the expected fields exist.
type PCFCertificate struct {
InstanceID, OrgID, SpaceID, AppID string
IPAddress net.IP
}
func (c *PCFCertificate) validate() error {
if c.InstanceID == "" {
return errors.New("no instance ID on given certificate")
}
if c.AppID == "" {
return errors.New("no app ID on given certificate")
}
if c.OrgID == "" {
return errors.New("no org ID on given certificate")
}
if c.SpaceID == "" {
return errors.New("no space ID on given certificate")
}
if c.IPAddress.IsUnspecified() {
return errors.New("ip address is unspecified")
}
return nil
}

View File

@ -0,0 +1,21 @@
package models
import (
"time"
"github.com/hashicorp/go-sockaddr"
)
// RoleEntry is a role as it's reflected in Vault's storage system.
type RoleEntry struct {
BoundAppIDs []string `json:"bound_application_ids"`
BoundSpaceIDs []string `json:"bound_space_ids"`
BoundOrgIDs []string `json:"bound_organization_ids"`
BoundInstanceIDs []string `json:"bound_instance_ids"`
BoundCIDRs []*sockaddr.SockAddrMarshaler `json:"bound_cidrs"`
Policies []string `json:"policies"`
DisableIPMatching bool `json:"disable_ip_matching"`
TTL time.Duration `json:"ttl"`
MaxTTL time.Duration `json:"max_ttl"`
Period time.Duration `json:"period"`
}

View File

@ -0,0 +1,239 @@
package pcf
import (
"context"
"fmt"
"strings"
"time"
"github.com/hashicorp/vault-plugin-auth-pcf/models"
"github.com/hashicorp/vault-plugin-auth-pcf/util"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
const configStorageKey = "config"
func (b *backend) pathConfig() *framework.Path {
return &framework.Path{
Pattern: "config",
Fields: map[string]*framework.FieldSchema{
"identity_ca_certificates": {
Required: true,
Type: framework.TypeStringSlice,
DisplayName: "Identity CA Certificates",
DisplayValue: `-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----`,
Description: "The PEM-format CA certificates that are required to have issued the instance certificates presented for logging in.",
},
"pcf_api_trusted_certificates": {
Type: framework.TypeStringSlice,
DisplayName: "PCF API Trusted IdentityCACertificates",
DisplayValue: `-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----`,
Description: "The PEM-format CA certificates that are acceptable for the PCF API to present.",
},
"pcf_api_addr": {
Required: true,
Type: framework.TypeString,
DisplayName: "PCF API Address",
DisplayValue: "https://api.10.244.0.34.xip.io",
Description: "PCFs API address.",
},
"pcf_username": {
Required: true,
Type: framework.TypeString,
DisplayName: "PCF API Username",
DisplayValue: "admin",
Description: "The username for PCFs API.",
},
"pcf_password": {
Required: true,
Type: framework.TypeString,
DisplayName: "PCF API Password",
DisplayValue: "admin",
Description: "The password for PCFs API.",
DisplaySensitive: true,
},
"login_max_seconds_old": {
Type: framework.TypeDurationSecond,
DisplayName: "Login Max Seconds Old",
DisplayValue: "300",
Description: `Duration in seconds for the maximum acceptable age of a "signing_time". Useful for clock drift.
Set low to reduce the opportunity for replay attacks.`,
Default: 300,
},
"login_max_seconds_ahead": {
Type: framework.TypeInt,
DisplayName: "Login Max Seconds Ahead",
DisplayValue: "60",
Description: `Duration in seconds for the maximum acceptable length in the future a "signing_time" can be. Useful for clock drift.
Set low to reduce the opportunity for replay attacks.`,
Default: 60,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.CreateOperation: &framework.PathOperation{
Callback: b.operationConfigCreateUpdate,
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.operationConfigCreateUpdate,
},
logical.ReadOperation: &framework.PathOperation{
Callback: b.operationConfigRead,
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.operationConfigDelete,
},
},
HelpSynopsis: pathConfigSyn,
HelpDescription: pathConfigDesc,
}
}
func (b *backend) operationConfigCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
// They're creating a config.
identityCACerts := data.Get("identity_ca_certificates").([]string)
if len(identityCACerts) == 0 {
return logical.ErrorResponse("'identity_ca_certificates' is required"), nil
}
pcfApiAddr := data.Get("pcf_api_addr").(string)
if pcfApiAddr == "" {
return logical.ErrorResponse("'pcf_api_addr' is required"), nil
}
pcfUsername := data.Get("pcf_username").(string)
if pcfUsername == "" {
return logical.ErrorResponse("'pcf_username' is required"), nil
}
pcfPassword := data.Get("pcf_password").(string)
if pcfPassword == "" {
return logical.ErrorResponse("'pcf_password' is required"), nil
}
pcfApiCertificates := data.Get("pcf_api_trusted_certificates").([]string)
// Default this to 5 minutes.
loginMaxSecOld := 300 * time.Second
if raw, ok := data.GetOk("login_max_seconds_old"); ok {
loginMaxSecOld = time.Duration(raw.(int)) * time.Second
}
// Default this to 1 minute.
loginMaxSecAhead := 60 * time.Second
if raw, ok := data.GetOk("login_max_seconds_ahead"); ok {
loginMaxSecAhead = time.Duration(raw.(int)) * time.Second
}
config = &models.Configuration{
IdentityCACertificates: identityCACerts,
PCFAPICertificates: pcfApiCertificates,
PCFAPIAddr: pcfApiAddr,
PCFUsername: pcfUsername,
PCFPassword: pcfPassword,
LoginMaxSecOld: loginMaxSecOld,
LoginMaxSecAhead: loginMaxSecAhead,
}
} else {
// They're updating a config. Only update the fields that have been sent in the call.
if raw, ok := data.GetOk("identity_ca_certificates"); ok {
config.IdentityCACertificates = raw.([]string)
}
if raw, ok := data.GetOk("pcf_api_trusted_certificates"); ok {
config.PCFAPICertificates = raw.([]string)
}
if raw, ok := data.GetOk("pcf_api_addr"); ok {
config.PCFAPIAddr = raw.(string)
}
if raw, ok := data.GetOk("pcf_username"); ok {
config.PCFUsername = raw.(string)
}
if raw, ok := data.GetOk("pcf_password"); ok {
config.PCFPassword = raw.(string)
}
if raw, ok := data.GetOk("login_max_seconds_old"); ok {
config.LoginMaxSecOld = time.Duration(raw.(int)) * time.Second
}
if raw, ok := data.GetOk("login_max_seconds_ahead"); ok {
config.LoginMaxSecAhead = time.Duration(raw.(int)) * time.Second
}
}
// To give early and explicit feedback, make sure the config works by executing a test call
// and checking that the API version is supported. If they don't have API v2 running, we would
// probably expect a timeout of some sort below because it's first called in the NewPCFClient
// method.
client, err := util.NewPCFClient(config)
if err != nil {
return nil, fmt.Errorf("unable to establish an initial connection to the PCF API: %s", err)
}
info, err := client.GetInfo()
if err != nil {
return nil, err
}
if !strings.HasPrefix(info.APIVersion, "2.") {
return nil, fmt.Errorf("the PCF auth plugin only supports version 2.X.X of the PCF API")
}
entry, err := logical.StorageEntryJSON(configStorageKey, config)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
return nil, nil
}
func (b *backend) operationConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"identity_ca_certificates": config.IdentityCACertificates,
"pcf_api_trusted_certificates": config.PCFAPICertificates,
"pcf_api_addr": config.PCFAPIAddr,
"pcf_username": config.PCFUsername,
"login_max_seconds_old": config.LoginMaxSecOld / time.Second,
"login_max_seconds_ahead": config.LoginMaxSecAhead / time.Second,
},
}, nil
}
func (b *backend) operationConfigDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
if err := req.Storage.Delete(ctx, configStorageKey); err != nil {
return nil, err
}
return nil, nil
}
// storedConfig may return nil without error if the user doesn't currently have a config.
func config(ctx context.Context, storage logical.Storage) (*models.Configuration, error) {
entry, err := storage.Get(ctx, configStorageKey)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
config := &models.Configuration{}
if err := entry.DecodeJSON(config); err != nil {
return nil, err
}
return config, nil
}
const pathConfigSyn = `
Provide Vault with the CA certificate used to issue all client certificates.
`
const pathConfigDesc = `
When a login is attempted using a PCF client certificate, Vault will verify
that the client certificate was issued by the CA certificate configured here.
Only those passing this check will be able to gain authorization.
`

View File

@ -0,0 +1,382 @@
package pcf
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/hashicorp/vault-plugin-auth-pcf/models"
"github.com/hashicorp/vault-plugin-auth-pcf/signatures"
"github.com/hashicorp/vault-plugin-auth-pcf/util"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/cidrutil"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/logical"
"github.com/pkg/errors"
)
func (b *backend) pathLogin() *framework.Path {
return &framework.Path{
Pattern: "login",
Fields: map[string]*framework.FieldSchema{
"role": {
Required: true,
Type: framework.TypeString,
DisplayName: "Role Name",
DisplayValue: "internally-defined-role",
Description: "The name of the role to authenticate against.",
},
"cf_instance_cert": {
Required: true,
Type: framework.TypeString,
DisplayName: "CF_INSTANCE_CERT Contents",
Description: "The full body of the file available at the CF_INSTANCE_CERT path on the PCF instance.",
},
"signing_time": {
Required: true,
Type: framework.TypeString,
DisplayName: "Signing Time",
DisplayValue: "2006-01-02T15:04:05Z",
Description: "The date and time used to construct the signature.",
},
"signature": {
Required: true,
Type: framework.TypeString,
DisplayName: "Signature",
Description: "The signature generated by the client certificate's private key.",
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.UpdateOperation: &framework.PathOperation{
Callback: b.operationLoginUpdate,
},
},
HelpSynopsis: pathLoginSyn,
HelpDescription: pathLoginDesc,
}
}
// operationLoginUpdate is called by those wanting to gain access to Vault.
// They present the instance certificates that should have been issued by the pre-configured
// Certificate Authority, and a signature that should have been signed by the instance cert's
// private key. If this holds true, there are additional checks verifying everything looks
// good before authentication is given.
func (b *backend) operationLoginUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Grab the time immediately for checking against the request's signingTime.
timeReceived := time.Now().UTC()
roleName := data.Get("role").(string)
if roleName == "" {
return logical.ErrorResponse("'role-name' is required"), nil
}
signature := data.Get("signature").(string)
if signature == "" {
return logical.ErrorResponse("'signature' is required"), nil
}
cfInstanceCertContents := data.Get("cf_instance_cert").(string)
if cfInstanceCertContents == "" {
return logical.ErrorResponse("'cf_instance_cert' is required"), nil
}
signingTimeRaw := data.Get("signing_time").(string)
if signingTimeRaw == "" {
return logical.ErrorResponse("'signing_time' is required"), nil
}
signingTime, err := parseTime(signingTimeRaw)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
config, err := config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, errors.New("no CA is configured for verifying client certificates")
}
// Ensure the time it was signed isn't too far in the past or future.
oldestAllowableSigningTime := timeReceived.Add(-1 * config.LoginMaxSecOld)
furthestFutureAllowableSigningTime := timeReceived.Add(config.LoginMaxSecAhead)
if signingTime.Before(oldestAllowableSigningTime) {
return logical.ErrorResponse(fmt.Sprintf("request is too old; signed at %s but received request at %s; allowable seconds old is %d", signingTime, timeReceived, config.LoginMaxSecOld/time.Second)), nil
}
if signingTime.After(furthestFutureAllowableSigningTime) {
return logical.ErrorResponse(fmt.Sprintf("request is too far in the future; signed at %s but received request at %s; allowable seconds in the future is %d", signingTime, timeReceived, config.LoginMaxSecAhead/time.Second)), nil
}
intermediateCert, identityCert, err := util.ExtractCertificates(cfInstanceCertContents)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Ensure the private key used to create the signature matches our identity
// certificate, and that it signed the same data as is presented in the body.
// This offers some protection against MITM attacks.
signingCert, err := signatures.Verify(signature, &signatures.SignatureData{
SigningTime: signingTime,
Role: roleName,
CFInstanceCertContents: cfInstanceCertContents,
})
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Make sure the identity/signing cert was actually issued by our CA.
if err := util.Validate(config.IdentityCACertificates, intermediateCert, identityCert, signingCert); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Read PCF's identity fields from the certificate.
pcfCert, err := models.NewPCFCertificateFromx509(signingCert)
if err != nil {
return nil, err
}
// It may help some users to be able to easily view the incoming certificate information
// in an un-encoded format, as opposed to the encoded format that will appear in the Vault
// audit logs.
if b.Logger().IsDebug() {
b.Logger().Debug(fmt.Sprintf("handling login attempt from %+v", pcfCert))
}
// Ensure the pcf certificate meets the role's constraints.
role, err := getRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return nil, errors.New("no matching role")
}
if err := b.validate(config, role, pcfCert, req.Connection.RemoteAddr); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Everything checks out.
return &logical.Response{
Auth: &logical.Auth{
Period: role.Period,
Policies: role.Policies,
InternalData: map[string]interface{}{
"role": roleName,
"instance_id": pcfCert.InstanceID,
"ip_address": pcfCert.IPAddress.String(),
},
DisplayName: pcfCert.InstanceID,
LeaseOptions: logical.LeaseOptions{
Renewable: true,
TTL: role.TTL,
MaxTTL: role.MaxTTL,
},
Alias: &logical.Alias{
Name: pcfCert.AppID,
Metadata: map[string]string{
"org_id": pcfCert.OrgID,
"app_id": pcfCert.AppID,
"space_id": pcfCert.SpaceID,
},
},
BoundCIDRs: role.BoundCIDRs,
},
}, nil
}
func (b *backend) pathLoginRenew(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
config, err := config(ctx, req.Storage)
if err != nil {
return nil, err
}
if config == nil {
return nil, errors.New("no configuration is available for reaching the PCF API")
}
roleName, err := getOrErr("role", req.Auth.InternalData)
if err != nil {
return nil, err
}
role, err := getRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return nil, errors.New("no matching role")
}
instanceID, err := getOrErr("instance_id", req.Auth.InternalData)
if err != nil {
return nil, err
}
ipAddr, err := getOrErr("ip_address", req.Auth.InternalData)
if err != nil {
return nil, err
}
orgID, err := getOrErr("org_id", req.Auth.Alias.Metadata)
if err != nil {
return nil, err
}
spaceID, err := getOrErr("space_id", req.Auth.Alias.Metadata)
if err != nil {
return nil, err
}
appID, err := getOrErr("app_id", req.Auth.Alias.Metadata)
if err != nil {
return nil, err
}
// Reconstruct the certificate and ensure it still meets all constraints.
pcfCert, err := models.NewPCFCertificate(instanceID, orgID, spaceID, appID, ipAddr)
if err := b.validate(config, role, pcfCert, req.Connection.RemoteAddr); err != nil {
return logical.ErrorResponse(err.Error()), nil
}
resp := &logical.Response{Auth: req.Auth}
resp.Auth.TTL = role.TTL
resp.Auth.MaxTTL = role.MaxTTL
resp.Auth.Period = role.Period
return resp, nil
}
func (b *backend) validate(config *models.Configuration, role *models.RoleEntry, pcfCert *models.PCFCertificate, reqConnRemoteAddr string) error {
if !role.DisableIPMatching {
if !matchesIPAddress(reqConnRemoteAddr, pcfCert.IPAddress) {
return errors.New("no matching IP address")
}
}
if !meetsBoundConstraints(pcfCert.InstanceID, role.BoundInstanceIDs) {
return fmt.Errorf("instance ID %s doesn't match role constraints of %s", pcfCert.InstanceID, role.BoundInstanceIDs)
}
if !meetsBoundConstraints(pcfCert.AppID, role.BoundAppIDs) {
return fmt.Errorf("app ID %s doesn't match role constraints of %s", pcfCert.AppID, role.BoundAppIDs)
}
if !meetsBoundConstraints(pcfCert.OrgID, role.BoundOrgIDs) {
return fmt.Errorf("org ID %s doesn't match role constraints of %s", pcfCert.OrgID, role.BoundOrgIDs)
}
if !meetsBoundConstraints(pcfCert.SpaceID, role.BoundSpaceIDs) {
return fmt.Errorf("space ID %s doesn't match role constraints of %s", pcfCert.SpaceID, role.BoundSpaceIDs)
}
if !cidrutil.RemoteAddrIsOk(reqConnRemoteAddr, role.BoundCIDRs) {
return fmt.Errorf("remote address %s doesn't match role constraints of %s", reqConnRemoteAddr, role.BoundCIDRs)
}
// Use the PCF API to ensure everything still exists and to verify whatever we can.
client, err := util.NewPCFClient(config)
if err != nil {
return err
}
// Here, if it were possible, we _would_ do an API call to check the instance ID,
// but currently there's no known way to do that via the pcf API.
// Check everything we can using the app ID.
app, err := client.AppByGuid(pcfCert.AppID)
if err != nil {
return err
}
if app.Guid != pcfCert.AppID {
return fmt.Errorf("cert app ID %s doesn't match API's expected one of %s", pcfCert.AppID, app.Guid)
}
if app.SpaceGuid != pcfCert.SpaceID {
return fmt.Errorf("cert space ID %s doesn't match API's expected one of %s", pcfCert.SpaceID, app.SpaceGuid)
}
if app.Instances <= 0 {
return errors.New("app doesn't have any live instances")
}
// Check everything we can using the org ID.
org, err := client.GetOrgByGuid(pcfCert.OrgID)
if err != nil {
return err
}
if org.Guid != pcfCert.OrgID {
return fmt.Errorf("cert org ID %s doesn't match API's expected one of %s", pcfCert.OrgID, org.Guid)
}
// Check everything we can using the space ID.
space, err := client.GetSpaceByGuid(pcfCert.SpaceID)
if err != nil {
return err
}
if space.Guid != pcfCert.SpaceID {
return fmt.Errorf("cert space ID %s doesn't match API's expected one of %s", pcfCert.SpaceID, space.Guid)
}
if space.OrganizationGuid != pcfCert.OrgID {
return fmt.Errorf("cert org ID %s doesn't match API's expected one of %s", pcfCert.OrgID, space.OrganizationGuid)
}
return nil
}
func meetsBoundConstraints(certValue string, constraints []string) bool {
if len(constraints) == 0 {
// There are no restrictions, so everything passes this check.
return true
}
// Check whether we have a match.
return strutil.StrListContains(constraints, certValue)
}
func matchesIPAddress(remoteAddr string, certIP net.IP) bool {
// Some remote addresses may arrive like "10.255.181.105/32"
// but the certificate will only have the IP address without
// the subnet mask, so that's what we want to match against.
// For those wanting to also match the subnet, use bound_cidrs.
parts := strings.Split(remoteAddr, "/")
reqIPAddr := net.ParseIP(parts[0])
if certIP.Equal(reqIPAddr) {
return true
}
return false
}
// Try parsing this as ISO 8601 AND the way that is default provided by Bash to make it easier to give via the CLI as well.
func parseTime(signingTime string) (time.Time, error) {
if signingTime, err := time.Parse(signatures.TimeFormat, signingTime); err == nil {
return signingTime, nil
}
if signingTime, err := time.Parse(util.BashTimeFormat, signingTime); err == nil {
return signingTime, nil
}
return time.Time{}, fmt.Errorf("couldn't parse %s", signingTime)
}
// getOrErr is a convenience method for pulling a string from a map.
func getOrErr(fieldName string, from interface{}) (string, error) {
switch givenMap := from.(type) {
case map[string]interface{}:
vIfc, ok := givenMap[fieldName]
if !ok {
return "", fmt.Errorf("unable to retrieve %q during renewal", fieldName)
}
v, ok := vIfc.(string)
if v == "" {
return "", fmt.Errorf("unable to retrieve %q during renewal, not a string", fieldName)
}
return v, nil
case map[string]string:
v, ok := givenMap[fieldName]
if !ok {
return "", fmt.Errorf("unable to retrieve %q during renewal", fieldName)
}
return v, nil
default:
return "", fmt.Errorf("unrecognized type for structure containing %s", fieldName)
}
}
const pathLoginSyn = `
Authenticates an entity with Vault.
`
const pathLoginDesc = `
Authenticate PCF entities using a client certificate issued by the
configured Certificate Authority, and signed by a client key belonging
to the client certificate.
`

View File

@ -0,0 +1,275 @@
package pcf
import (
"context"
"fmt"
"time"
"github.com/hashicorp/vault-plugin-auth-pcf/models"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/parseutil"
"github.com/hashicorp/vault/sdk/logical"
)
const roleStoragePrefix = "roles/"
func (b *backend) pathListRoles() *framework.Path {
return &framework.Path{
Pattern: "roles/?$",
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: b.operationRolesList,
},
},
HelpSynopsis: pathListRolesHelpSyn,
HelpDescription: pathListRolesHelpDesc,
}
}
func (b *backend) operationRolesList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
entries, err := req.Storage.List(ctx, roleStoragePrefix)
if err != nil {
return nil, err
}
return logical.ListResponse(entries), nil
}
func (b *backend) pathRoles() *framework.Path {
return &framework.Path{
Pattern: "roles/" + framework.GenericNameRegex("role"),
Fields: map[string]*framework.FieldSchema{
"role": {
Type: framework.TypeLowerCaseString,
Required: true,
Description: "The name of the role.",
},
"bound_application_ids": {
Type: framework.TypeCommaStringSlice,
DisplayName: "Bound Application IDs",
DisplayValue: "6b814521-5f08-4b1a-8c4e-fbe7c5f3a169",
Description: "Require that the client certificate presented has at least one of these app IDs.",
},
"bound_space_ids": {
Type: framework.TypeCommaStringSlice,
DisplayName: "Bound Space IDs",
DisplayValue: "3d2eba6b-ef19-44d5-91dd-1975b0db5cc9",
Description: "Require that the client certificate presented has at least one of these space IDs.",
},
"bound_organization_ids": {
Type: framework.TypeCommaStringSlice,
DisplayName: "Bound Organization IDs",
DisplayValue: "34a878d0-c2f9-4521-ba73-a9f664e82c7b",
Description: "Require that the client certificate presented has at least one of these org IDs.",
},
"bound_instance_ids": {
Type: framework.TypeCommaStringSlice,
DisplayName: "Bound Instance IDs",
DisplayValue: "8a886b31-ccf7-480d-54d8-cc28",
Description: "Require that the client certificate presented has at least one of these instance IDs.",
},
"bound_cidrs": {
Type: framework.TypeCommaStringSlice,
DisplayName: "Bound CIDRs",
DisplayValue: "192.168.100.14/24",
Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of
IP addresses which can perform the login operation.`,
},
"policies": {
Type: framework.TypeCommaStringSlice,
Default: "default",
DisplayName: "Policies",
DisplayValue: "default",
Description: "Comma separated list of policies on the role.",
},
"disable_ip_matching": {
Type: framework.TypeBool,
Default: false,
DisplayName: "Disable IP Address Matching",
DisplayValue: "false",
Description: `If set to true, disables the default behavior that logging in must be performed from
an acceptable IP address described by the certificate presented.`,
},
"ttl": {
Type: framework.TypeDurationSecond,
Description: `Duration in seconds after which the issued token should expire. Defaults
to 0, in which case the value will fallback to the system/mount defaults.`,
},
"max_ttl": {
Type: framework.TypeDurationSecond,
Description: "The maximum allowed lifetime of tokens issued using this role.",
},
"period": {
Type: framework.TypeDurationSecond,
Default: 0,
DisplayName: "Period",
Description: `If set, indicates that the token generated using this role
should never expire. The token should be renewed within the
duration specified by this value. At each renewal, the token's
TTL will be set to the value of this parameter.`,
},
},
ExistenceCheck: b.operationRolesExistenceCheck,
Operations: map[logical.Operation]framework.OperationHandler{
logical.CreateOperation: &framework.PathOperation{
Callback: b.operationRolesCreateUpdate,
},
logical.UpdateOperation: &framework.PathOperation{
Callback: b.operationRolesCreateUpdate,
},
logical.ReadOperation: &framework.PathOperation{
Callback: b.operationRolesRead,
},
logical.DeleteOperation: &framework.PathOperation{
Callback: b.operationRolesDelete,
},
},
HelpSynopsis: pathRolesHelpSyn,
HelpDescription: pathRolesHelpDesc,
}
}
func (b *backend) operationRolesExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
entry, err := req.Storage.Get(ctx, roleStoragePrefix+data.Get("role").(string))
if err != nil {
return false, err
}
return entry != nil, nil
}
func (b *backend) operationRolesCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("role").(string)
role := &models.RoleEntry{}
if req.Operation == logical.UpdateOperation {
storedRole, err := getRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if storedRole != nil {
role = storedRole
}
}
if raw, ok := data.GetOk("bound_application_ids"); ok {
role.BoundAppIDs = raw.([]string)
}
if raw, ok := data.GetOk("bound_space_ids"); ok {
role.BoundSpaceIDs = raw.([]string)
}
if raw, ok := data.GetOk("bound_organization_ids"); ok {
role.BoundOrgIDs = raw.([]string)
}
if raw, ok := data.GetOk("bound_instance_ids"); ok {
role.BoundInstanceIDs = raw.([]string)
}
if raw, ok := data.GetOk("bound_cidrs"); ok {
parsedCIDRs, err := parseutil.ParseAddrs(raw)
if err != nil {
return nil, err
}
role.BoundCIDRs = parsedCIDRs
}
if raw, ok := data.GetOk("policies"); ok {
role.Policies = raw.([]string)
}
if raw, ok := data.GetOk("disable_ip_matching"); ok {
role.DisableIPMatching = raw.(bool)
}
if raw, ok := data.GetOk("ttl"); ok {
role.TTL = time.Duration(raw.(int)) * time.Second
}
if raw, ok := data.GetOk("max_ttl"); ok {
role.MaxTTL = time.Duration(raw.(int)) * time.Second
}
if raw, ok := data.GetOk("period"); ok {
role.Period = time.Duration(raw.(int)) * time.Second
}
if role.MaxTTL > 0 && role.TTL > role.MaxTTL {
return logical.ErrorResponse("ttl exceeds max_ttl"), nil
}
entry, err := logical.StorageEntryJSON(roleStoragePrefix+roleName, role)
if err != nil {
return nil, err
}
if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}
if role.TTL > b.System().MaxLeaseTTL() {
resp := &logical.Response{}
resp.AddWarning(fmt.Sprintf("ttl of %d exceeds the system max ttl of %d, the latter will be used during login", role.TTL, b.System().MaxLeaseTTL()))
return resp, nil
}
return nil, nil
}
func (b *backend) operationRolesRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("role").(string)
role, err := getRole(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
cidrs := make([]string, len(role.BoundCIDRs))
for i, cidr := range role.BoundCIDRs {
cidrs[i] = cidr.String()
}
return &logical.Response{
Data: map[string]interface{}{
"bound_application_ids": role.BoundAppIDs,
"bound_space_ids": role.BoundSpaceIDs,
"bound_organization_ids": role.BoundOrgIDs,
"bound_instance_ids": role.BoundInstanceIDs,
"bound_cidrs": cidrs,
"policies": role.Policies,
"disable_ip_matching": role.DisableIPMatching,
"ttl": role.TTL / time.Second,
"max_ttl": role.MaxTTL / time.Second,
"period": role.Period / time.Second,
},
}, nil
}
func (b *backend) operationRolesDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("role").(string)
if err := req.Storage.Delete(ctx, roleStoragePrefix+roleName); err != nil {
return nil, err
}
return nil, nil
}
func getRole(ctx context.Context, storage logical.Storage, roleName string) (*models.RoleEntry, error) {
r := &models.RoleEntry{}
entry, err := storage.Get(ctx, roleStoragePrefix+roleName)
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
if err := entry.DecodeJSON(r); err != nil {
return nil, err
}
return r, nil
}
const pathListRolesHelpSyn = "List the existing roles in this backend."
const pathListRolesHelpDesc = "Roles will be listed by the role name."
const pathRolesHelpSyn = `
Read, write and reference policies and roles that tokens can be made for.
`
const pathRolesHelpDesc = `
This path allows you to read and write roles that are used to
create Vault tokens.
Once configured, credentials will be able to be obtained using this role name
if the caller can successfully provide a client certificate, and sign it
using a valid secret key. The client certificate provided must have been issued
by the configured certficate authority. Its parameters must also match anything
you've listed as "bound".
`

View File

@ -0,0 +1,118 @@
package signatures
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"time"
"github.com/hashicorp/go-multierror"
)
const TimeFormat = "2006-01-02T15:04:05Z"
type SignatureData struct {
SigningTime time.Time
Role string
// CFInstanceCertContents are the full contents/body of the file
// available at CF_INSTANCE_CERT. When viewed visually, this file
// will contain two certificates. Generally, the first one is the
// identity certificate itself, and the second one is the intermediate
// certificate that issued it.
CFInstanceCertContents string
}
func (s *SignatureData) hash() []byte {
sum := sha256.Sum256([]byte(s.toSign()))
return sum[:]
}
func (s *SignatureData) toSign() string {
toHash := ""
for _, field := range []string{s.SigningTime.UTC().Format(TimeFormat), s.CFInstanceCertContents, s.Role} {
toHash += field
}
return toHash
}
func Sign(pathToPrivateKey string, signatureData *SignatureData) (string, error) {
if signatureData == nil {
return "", errors.New("signatureData must be provided")
}
keyBytes, err := ioutil.ReadFile(pathToPrivateKey)
if err != nil {
return "", err
}
block, _ := pem.Decode(keyBytes)
if block == nil {
return "", fmt.Errorf("unable to decode RSA private key from %s", keyBytes)
}
rsaPrivateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return "", err
}
signatureBytes, err := rsa.SignPSS(rand.Reader, rsaPrivateKey, crypto.SHA256, signatureData.hash(), nil)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(signatureBytes), nil
}
// Verify ensures that a given signature was created by a private key
// matching one of the given instance certificates. It returns the matching
// certificate, which should further be verified to be the identity certificate,
// and to be issued by a chain leading to the root CA certificate. There's a
// util function for this named Validate.
func Verify(signature string, signatureData *SignatureData) (*x509.Certificate, error) {
if signatureData == nil {
return nil, errors.New("signatureData must be provided")
}
// Use the CA certificate to verify the signature we've received.
signatureBytes, err := base64.URLEncoding.DecodeString(signature)
if err != nil {
return nil, err
}
cfInstanceCertContentsBytes := []byte(signatureData.CFInstanceCertContents)
var block *pem.Block
var result error
for {
block, cfInstanceCertContentsBytes = pem.Decode(cfInstanceCertContentsBytes)
if block == nil {
break
}
instanceCerts, err := x509.ParseCertificates(block.Bytes)
if err != nil {
result = multierror.Append(result, err)
continue
}
for _, instanceCert := range instanceCerts {
publicKey, ok := instanceCert.PublicKey.(*rsa.PublicKey)
if !ok {
result = multierror.Append(result, fmt.Errorf("not an rsa public key, it's a %t", instanceCert.PublicKey))
continue
}
if err := rsa.VerifyPSS(publicKey, crypto.SHA256, signatureData.hash(), signatureBytes, nil); err != nil {
result = multierror.Append(result, err)
continue
}
// Success
return instanceCert, nil
}
}
if result == nil {
return nil, fmt.Errorf("no matching certificate found for %s in %s", signature, signatureData.CFInstanceCertContents)
}
return nil, result
}

View File

@ -0,0 +1,282 @@
package certificates
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"math/big"
"net"
"os"
"time"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/go-uuid"
)
// Generate is a convenience method for testing. It creates a group of test certificates with the
// client certificate reflecting the given values. Close() should be called when done to immediately
// delete the three temporary files it has created.
//
// Usage:
//
// testCerts, err := certificates.Generate(...)
// if err != nil {
// ...
// }
// defer func(){
// if err := testCerts.Close(); err != nil {
// ...
// }
// }()
//
func Generate(instanceID, orgID, spaceID, appID, ipAddress string) (*TestCertificates, error) {
caCert, instanceCert, instanceKey, err := generate(instanceID, orgID, spaceID, appID, ipAddress)
if err != nil {
return nil, err
}
// Keep a list of paths we've created so that if we fail along the way,
// we can attempt to clean them up.
var paths []string
pathToCACertificate, err := makePathTo(caCert)
if err != nil {
// No path was successfully created, so we don't need to cleanup here.
return nil, err
}
paths = append(paths, pathToCACertificate)
pathToInstanceCertificate, err := makePathTo(instanceCert)
if err != nil {
if cleanupErr := cleanup(paths); cleanupErr != nil {
return nil, multierror.Append(err, cleanupErr)
}
return nil, err
}
paths = append(paths, pathToInstanceCertificate)
pathToInstanceKey, err := makePathTo(instanceKey)
if err != nil {
if cleanupErr := cleanup(paths); cleanupErr != nil {
return nil, multierror.Append(err, cleanupErr)
}
return nil, err
}
paths = append(paths, pathToInstanceKey)
// Provide a function to be called at the end cleaning up our temporary files.
cleanup := func() error {
return cleanup(paths)
}
return &TestCertificates{
CACertificate: caCert,
InstanceCertificate: instanceCert,
InstanceKey: instanceKey,
PathToCACertificate: pathToCACertificate,
PathToInstanceCertificate: pathToInstanceCertificate,
PathToInstanceKey: pathToInstanceKey,
cleanup: cleanup,
}, nil
}
type TestCertificates struct {
CACertificate string
InstanceCertificate string
InstanceKey string
PathToCACertificate string
PathToInstanceCertificate string
PathToInstanceKey string
// cleanup contains a function that has a path to all the temporary files we made,
// and deletes them. They're all in the /tmp folder so they'll disappear on the next
// system restart anyways, but in case of repeated tests, it's best to leave nothing
// behind if possible.
cleanup func() error
}
func (e *TestCertificates) Close() error {
return e.cleanup()
}
func generate(instanceID, orgID, spaceID, appID, ipAddress string) (caCert, instanceCert, instanceKey string, err error) {
caCert, caPriv, err := generateCA("", nil)
if err != nil {
return "", "", "", err
}
intermediateCert, intermediatePriv, err := generateCA(caCert, caPriv)
if err != nil {
return "", "", "", err
}
identityCert, identityPriv, err := generateIdentity(intermediateCert, intermediatePriv, instanceID, orgID, spaceID, appID, ipAddress)
if err != nil {
return "", "", "", err
}
// Convert the identity key to something appropriate for a file body.
out := &bytes.Buffer{}
pem.Encode(out, pemBlockForKey(identityPriv))
instanceKey = out.String()
return caCert, fmt.Sprintf("%s%s", intermediateCert, identityCert), instanceKey, nil
}
func generateCA(caCert string, caPriv *rsa.PrivateKey) (string, *rsa.PrivateKey, error) {
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"US"},
Province: []string{"CA"},
Organization: []string{"Testing, Inc."},
CommonName: "test-CA",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 100),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: true,
}
// Default to self-signing certificates by listing using itself as a parent.
parent := &template
// If a cert is provided, use it as the parent.
if caCert != "" {
block, certBytes := pem.Decode([]byte(caCert))
if block == nil {
return "", nil, errors.New("block shouldn't be nil")
}
if len(certBytes) > 0 {
return "", nil, errors.New("there shouldn't be more bytes")
}
ca509cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", nil, err
}
parent = ca509cert
}
// If a private key isn't provided, make a new one.
priv := caPriv
if priv == nil {
newPriv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", nil, err
}
priv = newPriv
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, parent, publicKey(priv), priv)
if err != nil {
return "", nil, err
}
out := &bytes.Buffer{}
pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
cert := out.String()
return cert, priv, nil
}
func generateIdentity(caCert string, caPriv *rsa.PrivateKey, instanceID, orgID, spaceID, appID, ipAddress string) (string, *rsa.PrivateKey, error) {
block, certBytes := pem.Decode([]byte(caCert))
if block == nil {
return "", nil, errors.New("block shouldn't be nil")
}
if len(certBytes) > 0 {
return "", nil, errors.New("there shouldn't be more bytes")
}
ca509cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return "", nil, err
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Country: []string{"US"},
Province: []string{"CA"},
Organization: []string{"Cloud Foundry"},
OrganizationalUnit: []string{
fmt.Sprintf("organization:%s", orgID),
fmt.Sprintf("space:%s", spaceID),
fmt.Sprintf("app:%s", appID),
},
CommonName: instanceID,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour * 24 * 365 * 100),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
IsCA: false,
IPAddresses: []net.IP{net.ParseIP(ipAddress)},
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return "", nil, err
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, ca509cert, publicKey(priv), caPriv)
if err != nil {
return "", nil, err
}
out := &bytes.Buffer{}
pem.Encode(out, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
cert := out.String()
return cert, priv, nil
}
func makePathTo(certOrKey string) (string, error) {
u, err := uuid.GenerateUUID()
if err != nil {
return "", err
}
tmpFile, err := ioutil.TempFile("", u)
if err != nil {
return "", err
}
if _, err := tmpFile.Write([]byte(certOrKey)); err != nil {
return "", err
}
if err := tmpFile.Close(); err != nil {
return "", err
}
return tmpFile.Name(), nil
}
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
default:
return nil
}
}
func pemBlockForKey(priv interface{}) *pem.Block {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)}
default:
return nil
}
}
func cleanup(paths []string) error {
var result error
for i := 0; i < len(paths); i++ {
if err := os.Remove(paths[i]); err != nil {
result = multierror.Append(result, err)
}
}
return result
}

View File

@ -0,0 +1,292 @@
package pcf
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/hashicorp/go-hclog"
)
const (
AuthUsername = "username"
AuthPassword = "password"
FoundServiceGUID = "1bf2e7f6-2d1d-41ec-501c-c70"
FoundAppGUID = "2d3e834a-3a25-4591-974c-fa5626d5d0a1"
FoundOrgGUID = "34a878d0-c2f9-4521-ba73-a9f664e82c7bf"
FoundSpaceGUID = "3d2eba6b-ef19-44d5-91dd-1975b0db5cc9"
UnfoundServiceGUID = "service-id-unfound"
UnfoundAppGUID = "app-id-unfound"
UnfoundOrgID = "org-id-unfound"
UnfoundSpaceGUID = "space-id-unfound"
)
var (
testServerUrl = ""
logger = hclog.Default()
)
func MockServer(loud bool) *httptest.Server {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if loud {
logger.Info(fmt.Sprintf("%+v", r))
}
// Below, 200's are returned by default, but are included anyways for explicitness.
pathFields := strings.Split(r.URL.EscapedPath(), "/")
lastPathField := pathFields[len(pathFields)-1]
switch lastPathField {
case "token":
w.Header().Add("Content-Type", "application/json;charset=UTF-8")
w.WriteHeader(200)
w.Write([]byte(tokenResponse))
case "info":
w.WriteHeader(200)
w.Write([]byte(strings.Replace(infoResponse, "{{TEST_URL}}", testServerUrl, -1)))
case FoundServiceGUID:
w.WriteHeader(200)
w.Write([]byte(serviceInstanceResponse))
case UnfoundServiceGUID:
w.WriteHeader(404)
w.Write([]byte(unfoundServiceInstanceResponse))
case FoundAppGUID:
w.WriteHeader(200)
w.Write([]byte(appResponse))
case UnfoundAppGUID:
w.WriteHeader(404)
w.Write([]byte(unfoundAppResponse))
case FoundOrgGUID:
w.WriteHeader(200)
w.Write([]byte(orgResponse))
case UnfoundOrgID:
w.WriteHeader(404)
w.Write([]byte(unfoundOrgResponse))
case FoundSpaceGUID:
w.WriteHeader(200)
w.Write([]byte(spaceResponse))
case UnfoundSpaceGUID:
w.WriteHeader(404)
w.Write([]byte(unfoundSpaceResponse))
default:
w.WriteHeader(400)
w.Write([]byte(fmt.Sprintf("unexpected object identifier: %s", lastPathField)))
}
}))
testServerUrl = testServer.URL
return testServer
}
const (
tokenResponse = `{
"access_token": "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vdWFhLmRldi5jZmRldi5zaC90b2tlbl9rZXlzIiwia2lkIjoia2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIxM2NiMzAyYjFjNjY0MDdkOWY3MDM2YzJjMmUxZDEyMCIsInN1YiI6IjYxMWM3ZWVhLWZmZDAtNGU5OC04MmYwLWY0YjU0YWZmNmRjYiIsInNjb3BlIjpbImNsaWVudHMucmVhZCIsIm9wZW5pZCIsInJvdXRpbmcucm91dGVyX2dyb3Vwcy53cml0ZSIsInNjaW0ucmVhZCIsImNsb3VkX2NvbnRyb2xsZXIuYWRtaW4iLCJ1YWEudXNlciIsInJvdXRpbmcucm91dGVyX2dyb3Vwcy5yZWFkIiwiY2xvdWRfY29udHJvbGxlci5yZWFkIiwicGFzc3dvcmQud3JpdGUiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwibmV0d29yay5hZG1pbiIsImRvcHBsZXIuZmlyZWhvc2UiLCJzY2ltLndyaXRlIl0sImNsaWVudF9pZCI6ImNmIiwiY2lkIjoiY2YiLCJhenAiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI2MTFjN2VlYS1mZmQwLTRlOTgtODJmMC1mNGI1NGFmZjZkY2IiLCJvcmlnaW4iOiJ1YWEiLCJ1c2VyX25hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW4iLCJhdXRoX3RpbWUiOjE1NTgzNzUwODksInJldl9zaWciOiIxOTA1YTEzOSIsImlhdCI6MTU1ODM3NTA4OSwiZXhwIjoxNTU4Mzc1Njg5LCJpc3MiOiJodHRwczovL3VhYS5kZXYuY2ZkZXYuc2gvb2F1dGgvdG9rZW4iLCJ6aWQiOiJ1YWEiLCJhdWQiOlsic2NpbSIsImNsb3VkX2NvbnRyb2xsZXIiLCJwYXNzd29yZCIsImNmIiwiY2xpZW50cyIsInVhYSIsIm9wZW5pZCIsImRvcHBsZXIiLCJyb3V0aW5nLnJvdXRlcl9ncm91cHMiLCJuZXR3b3JrIl19.KSdNhoQSTCh_3zJPLvxeAhEyAfVTvHN1mKprHqfDJJ79WaaEsUM-mLO68QWPvBgON5dx8dOE8GaQw--xpqpqNwncb7MN8jmz_lZxgw-6oOf_O-bYJmGsaxX-ETlMLKvuqUljSC5KvB16zBkRtAP2IhQsMOV-PGdx2Lz4CqBkzALHL4MUlnaaI6Z1O-zMVhFFunpmY-mYZqaHNw_35cNohieehq1TrrqVdHCiNkNVYi7LQPS93Ow8VC6I3GFNzNr6EAjmHu9tEq3sTKAfsBg8zEWjB_25cpiWW5gL-dPhZd4KSgp3wOh1K4kpWw7NKpLnPxf7mcRH4IgNDZPJqkqAjA",
"token_type": "bearer",
"id_token": "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vdWFhLmRldi5jZmRldi5zaC90b2tlbl9rZXlzIiwia2lkIjoia2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiI2MTFjN2VlYS1mZmQwLTRlOTgtODJmMC1mNGI1NGFmZjZkY2IiLCJhdWQiOlsiY2YiXSwiaXNzIjoiaHR0cHM6Ly91YWEuZGV2LmNmZGV2LnNoL29hdXRoL3Rva2VuIiwiZXhwIjoxNTU4Mzc1Njg5LCJpYXQiOjE1NTgzNzUwODksImFtciI6WyJwd2QiXSwiYXpwIjoiY2YiLCJzY29wZSI6WyJvcGVuaWQiXSwiZW1haWwiOiJhZG1pbiIsInppZCI6InVhYSIsIm9yaWdpbiI6InVhYSIsImp0aSI6IjEzY2IzMDJiMWM2NjQwN2Q5ZjcwMzZjMmMyZTFkMTIwIiwicHJldmlvdXNfbG9nb25fdGltZSI6MTU1ODM3NDk0NTEyMCwiZW1haWxfdmVyaWZpZWQiOnRydWUsImNsaWVudF9pZCI6ImNmIiwiY2lkIjoiY2YiLCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX25hbWUiOiJhZG1pbiIsInJldl9zaWciOiIxOTA1YTEzOSIsInVzZXJfaWQiOiI2MTFjN2VlYS1mZmQwLTRlOTgtODJmMC1mNGI1NGFmZjZkY2IiLCJhdXRoX3RpbWUiOjE1NTgzNzUwODl9.eOv9O17i1naYiycCwlXFu2Xh2xjBRNBagq61AX1y2Upb7ek42VFaAi92PAZN9rmcU9i3trvERen0Hv7aIottLM7U-MTKMBnHXjqr1fY5oWyWxGruWsM0T9RBu4g9dbs8hyqIh_be9KdiL4PSybChV7-RspF1kMa58OUvpgQbQhgOMMWKKODYVXeeY8z241octX_ST-5tZv_josk12sworPQbZCwA5QbUjmCNSc_fHg9xe4Ra_Wecq3hmmspHrHW8gTc6ggoWUzxbbCKo1rF2PIVHzJ_61cLaHBepax9DvhCYnSJtDjlG5lPy41dxc01dOAD-JLEaV-CigtrWntFUXQ",
"refresh_token": "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vdWFhLmRldi5jZmRldi5zaC90b2tlbl9rZXlzIiwia2lkIjoia2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJqdGkiOiIwOTRlYWQ0ZThiYWM0Nzk1ODJmMDI2ZmMwMjUwNTA2Yy1yIiwic3ViIjoiNjExYzdlZWEtZmZkMC00ZTk4LTgyZjAtZjRiNTRhZmY2ZGNiIiwiaWF0IjoxNTU4Mzc1MDg5LCJleHAiOjE1NjA5NjcwODksImNpZCI6ImNmIiwiY2xpZW50X2lkIjoiY2YiLCJpc3MiOiJodHRwczovL3VhYS5kZXYuY2ZkZXYuc2gvb2F1dGgvdG9rZW4iLCJ6aWQiOiJ1YWEiLCJhdWQiOlsic2NpbSIsImNsb3VkX2NvbnRyb2xsZXIiLCJwYXNzd29yZCIsImNmIiwiY2xpZW50cyIsInVhYSIsIm9wZW5pZCIsImRvcHBsZXIiLCJyb3V0aW5nLnJvdXRlcl9ncm91cHMiLCJuZXR3b3JrIl0sImdyYW50ZWRfc2NvcGVzIjpbImNsaWVudHMucmVhZCIsIm9wZW5pZCIsInJvdXRpbmcucm91dGVyX2dyb3Vwcy53cml0ZSIsInNjaW0ucmVhZCIsImNsb3VkX2NvbnRyb2xsZXIuYWRtaW4iLCJ1YWEudXNlciIsInJvdXRpbmcucm91dGVyX2dyb3Vwcy5yZWFkIiwiY2xvdWRfY29udHJvbGxlci5yZWFkIiwicGFzc3dvcmQud3JpdGUiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwibmV0d29yay5hZG1pbiIsImRvcHBsZXIuZmlyZWhvc2UiLCJzY2ltLndyaXRlIl0sImFtciI6WyJwd2QiXSwiYXV0aF90aW1lIjoxNTU4Mzc1MDg5LCJncmFudF90eXBlIjoicGFzc3dvcmQiLCJ1c2VyX25hbWUiOiJhZG1pbiIsIm9yaWdpbiI6InVhYSIsInVzZXJfaWQiOiI2MTFjN2VlYS1mZmQwLTRlOTgtODJmMC1mNGI1NGFmZjZkY2IiLCJyZXZfc2lnIjoiMTkwNWExMzkifQ.LFkoBtAWGL1x1bUo0ak16f-NeWpBS6NZspVwzaVhBv4xg7qxDryUayE5M2BQOGMb4tZLOU2cYyO2uu4li70u0LgJk7k3OZ0-hxKvjX4sJcoiLlJFCEsFzq_yG6iUFnA2w2kA70IQtACvAAHO--Jz0L1QGA8ebt20z7Rup0FufyDJFFevhbppzYb6AfghhnrB-yZbZU9rPq4Q8DWDTN0nMOBn05CA52NRKoj2157JXLRimEG7SZW6dhXUhdjbCvSz1WKiG6fS3fHK5ncqyQtuSqLfI0Naq1v77wfSzbvc0MB-IM4CPYc-ODhWbHFoV1z8kV6dWXm2ng7OyZe3u3A7Fw",
"expires_in": 599,
"scope": "clients.read openid routing.router_groups.write scim.read cloud_controller.admin uaa.user routing.router_groups.read cloud_controller.read password.write cloud_controller.write network.admin doppler.firehose scim.write",
"jti": "13cb302b1c66407d9f7036c2c2e1d120"
}`
infoResponse = `{
"name": "",
"build": "",
"support": "",
"version": 0,
"description": "",
"authorization_endpoint": "https://login.dev.cfdev.sh",
"token_endpoint": "{{TEST_URL}}",
"min_cli_version": null,
"min_recommended_cli_version": null,
"app_ssh_endpoint": "ssh.dev.cfdev.sh:2222",
"app_ssh_host_key_fingerprint": "96:4d:89:2d:39:18:bc:16:e1:d3:d8:44:f8:16:af:85",
"app_ssh_oauth_client": "ssh-proxy",
"doppler_logging_endpoint": "wss://doppler.dev.cfdev.sh:443",
"api_version": "2.133.0",
"osbapi_version": "2.14",
"routing_endpoint": "https://api.dev.cfdev.sh/routing",
"user": "611c7eea-ffd0-4e98-82f0-f4b54aff6dcb"
}`
serviceInstanceResponse = `{
"metadata": {
"guid": "1bf2e7f6-2d1d-41ec-501c-c70",
"url": "/v2/service_instances/1bf2e7f6-2d1d-41ec-501c-c70",
"created_at": "2016-06-08T16:41:29Z",
"updated_at": "2016-06-08T16:41:26Z"
},
"entity": {
"name": "name-1508",
"credentials": {
"creds-key-38": "creds-val-38"
},
"service_guid": "a14baddf-1ccc-5299-0152-ab9s49de4422",
"service_plan_guid": "779d2df0-9cdd-48e8-9781-ea05301cedb1",
"space_guid": "3d2eba6b-ef19-44d5-91dd-1975b0db5cc9",
"gateway_data": null,
"dashboard_url": null,
"type": "managed_service_instance",
"last_operation": {
"type": "create",
"state": "succeeded",
"description": "service broker-provided description",
"updated_at": "2016-06-08T16:41:29Z",
"created_at": "2016-06-08T16:41:29Z"
},
"tags": [
"accounting",
"mongodb"
],
"space_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9",
"service_url": "/v2/services/a14baddf-1ccc-5299-0152-ab9s49de4422",
"service_plan_url": "/v2/service_plans/779d2df0-9cdd-48e8-9781-ea05301cedb1",
"service_bindings_url": "/v2/service_instances/1bf2e7f6-2d1d-41ec-501c-c70/service_bindings",
"service_keys_url": "/v2/service_instances/1bf2e7f6-2d1d-41ec-501c-c70/service_keys",
"routes_url": "/v2/service_instances/1bf2e7f6-2d1d-41ec-501c-c70/routes",
"shared_from_url": "/v2/service_instances/1bf2e7f6-2d1d-41ec-501c-c70/shared_from",
"shared_to_url": "/v2/service_instances/1bf2e7f6-2d1d-41ec-501c-c70/shared_to",
"service_instance_parameters_url": "/v2/service_instances/1bf2e7f6-2d1d-41ec-501c-c70/parameters"
}
}`
unfoundServiceInstanceResponse = `{
"description": "The service instance could not be found: service-id-unfound",
"error_code": "CF-ServiceInstanceNotFound",
"code": 60004
}`
appResponse = `{
"metadata": {
"guid": "2d3e834a-3a25-4591-974c-fa5626d5d0a1",
"url": "/v2/apps/2d3e834a-3a25-4591-974c-fa5626d5d0a1",
"created_at": "2016-06-08T16:41:44Z",
"updated_at": "2016-06-08T16:41:44Z"
},
"entity": {
"name": "name-2401",
"production": false,
"space_guid": "3d2eba6b-ef19-44d5-91dd-1975b0db5cc9",
"stack_guid": "7e03186d-a438-4285-b3b7-c426532e1df2",
"buildpack": null,
"detected_buildpack": null,
"detected_buildpack_guid": null,
"environment_json": null,
"memory": 1024,
"instances": 1,
"disk_quota": 1024,
"state": "STOPPED",
"version": "df19a7ea-2003-4ecb-a909-e630e43f2719",
"command": null,
"console": false,
"debug": null,
"staging_task_id": null,
"package_state": "PENDING",
"health_check_http_endpoint": "",
"health_check_type": "port",
"health_check_timeout": null,
"staging_failed_reason": null,
"staging_failed_description": null,
"diego": false,
"docker_image": null,
"docker_credentials": {
"username": null,
"password": null
},
"package_updated_at": "2016-06-08T16:41:45Z",
"detected_start_command": "",
"enable_ssh": true,
"ports": null,
"space_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9",
"stack_url": "/v2/stacks/7e03186d-a438-4285-b3b7-c426532e1df2",
"routes_url": "/v2/apps/2d3e834a-3a25-4591-974c-fa5626d5d0a1/routes",
"events_url": "/v2/apps/2d3e834a-3a25-4591-974c-fa5626d5d0a1/events",
"service_bindings_url": "/v2/apps/2d3e834a-3a25-4591-974c-fa5626d5d0a1/service_bindings",
"route_mappings_url": "/v2/apps/2d3e834a-3a25-4591-974c-fa5626d5d0a1/route_mappings"
}
}`
unfoundAppResponse = `{
"description": "The app could not be found: app-id-unfound",
"error_code": "CF-AppNotFound",
"code": 100004
}`
orgResponse = `{
"metadata": {
"guid": "34a878d0-c2f9-4521-ba73-a9f664e82c7bf",
"url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf",
"created_at": "2019-05-17T22:49:40Z",
"updated_at": "2019-05-17T22:49:40Z"
},
"entity": {
"name": "system",
"billing_enabled": false,
"quota_definition_guid": "b172ff20-ae6d-4a13-a554-dc22f3844fb0",
"status": "active",
"default_isolation_segment_guid": null,
"quota_definition_url": "/v2/quota_definitions/b172ff20-ae6d-4a13-a554-dc22f3844fb0",
"spaces_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/spaces",
"domains_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/domains",
"private_domains_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/private_domains",
"users_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/users",
"managers_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/managers",
"billing_managers_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/billing_managers",
"auditors_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/auditors",
"app_events_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/app_events",
"space_quota_definitions_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf/space_quota_definitions"
}
}`
unfoundOrgResponse = `{
"description": "The organization could not be found: org-id-unfound",
"error_code": "CF-OrganizationNotFound",
"code": 30003
}`
spaceResponse = `{
"metadata": {
"guid": "3d2eba6b-ef19-44d5-91dd-1975b0db5cc9",
"url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9",
"created_at": "2019-05-17T22:53:30Z",
"updated_at": "2019-05-17T22:53:30Z"
},
"entity": {
"name": "cfdev-space",
"organization_guid": "34a878d0-c2f9-4521-ba73-a9f664e82c7bf",
"space_quota_definition_guid": null,
"isolation_segment_guid": null,
"allow_ssh": true,
"organization_url": "/v2/organizations/34a878d0-c2f9-4521-ba73-a9f664e82c7bf",
"developers_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/developers",
"managers_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/managers",
"auditors_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/auditors",
"apps_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/apps",
"routes_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/routes",
"domains_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/domains",
"service_instances_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/service_instances",
"app_events_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/app_events",
"events_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/events",
"security_groups_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/security_groups",
"staging_security_groups_url": "/v2/spaces/3d2eba6b-ef19-44d5-91dd-1975b0db5cc9/staging_security_groups"
}
}`
unfoundSpaceResponse = `{
"description": "The app space could not be found: space-id-unfound",
"error_code": "CF-SpaceNotFound",
"code": 40004
}`
)

View File

@ -0,0 +1,79 @@
package util
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"reflect"
"github.com/hashicorp/go-multierror"
)
// ExtractCertificates takes the contents of the file at CF_INSTANCE_CERT, which typically are
// comprised of two certificates. One is the identity certificate, and one is an intermediate
// CA certificate which is crucial in linking the identity cert back to the configured root
// certificate. It splits these two certificates apart, and identifies the certificate marked
// as a CA as the intermediate cert, and the one not marked as a CA as the identity certificate.
// It may error if the given file contents or certificates aren't as expected.
func ExtractCertificates(cfInstanceCertContents string) (intermediateCert, identityCert *x509.Certificate, err error) {
certPairBytes := []byte(cfInstanceCertContents)
numCerts := 0
var block *pem.Block
var result error
for {
block, certPairBytes = pem.Decode(certPairBytes)
if block == nil {
break
}
certs, err := x509.ParseCertificates(block.Bytes)
if err != nil {
result = multierror.Append(result, err)
continue
}
for _, cert := range certs {
if cert.IsCA {
intermediateCert = cert
} else {
identityCert = cert
}
numCerts++
}
}
if numCerts != 2 {
result = multierror.Append(fmt.Errorf("expected 2 certs but received %s", certPairBytes))
}
if intermediateCert == nil {
result = multierror.Append(fmt.Errorf("no intermediate certificate found in %s", certPairBytes))
}
if identityCert == nil {
result = multierror.Append(fmt.Errorf("no identity cert found in %s", certPairBytes))
}
return intermediateCert, identityCert, result
}
// Validate takes a group of trusted CA certificates, an intermediate certificate, an identity certificate,
// and a signing certificate, and makes sure they have the following properties:
// - The identity certificate is the same as the signing certificate
// - The identity certificate chains to at least one trusted CA
func Validate(caCerts []string, intermediateCert, identityCert, signingCert *x509.Certificate) error {
if !reflect.DeepEqual(identityCert, signingCert) {
return errors.New("signature not generated by identity cert")
}
roots := x509.NewCertPool()
for _, caCert := range caCerts {
if ok := roots.AppendCertsFromPEM([]byte(caCert)); !ok {
return errors.New("couldn't append root certificate")
}
}
intermediates := x509.NewCertPool()
intermediates.AddCert(intermediateCert)
verifyOpts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
}
if _, err := signingCert.Verify(verifyOpts); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,42 @@
package util
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"github.com/cloudfoundry-community/go-cfclient"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/vault-plugin-auth-pcf/models"
)
const BashTimeFormat = "Mon Jan 2 15:04:05 MST 2006"
// NewPCFClient does some work that's needed every time we use the PCF client,
// namely using cleanhttp and configuring it to match the user conf.
func NewPCFClient(config *models.Configuration) (*cfclient.Client, error) {
clientConf := &cfclient.Config{
ApiAddress: config.PCFAPIAddr,
Username: config.PCFUsername,
Password: config.PCFPassword,
HttpClient: cleanhttp.DefaultClient(),
}
rootCAs, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
if rootCAs == nil {
rootCAs = x509.NewCertPool()
}
for _, certificate := range config.PCFAPICertificates {
if ok := rootCAs.AppendCertsFromPEM([]byte(certificate)); !ok {
return nil, fmt.Errorf("couldn't append PCF API cert to trust: %s", certificate)
}
}
tlsConfig := &tls.Config{
RootCAs: rootCAs,
}
clientConf.HttpClient.Transport = &http.Transport{TLSClientConfig: tlsConfig}
return cfclient.NewClient(clientConf)
}

View File

@ -0,0 +1,120 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package clientcredentials implements the OAuth2.0 "client credentials" token flow,
// also known as the "two-legged OAuth 2.0".
//
// This should be used when the client is acting on its own behalf or when the client
// is the resource owner. It may also be used when requesting access to protected
// resources based on an authorization previously arranged with the authorization
// server.
//
// See https://tools.ietf.org/html/rfc6749#section-4.4
package clientcredentials // import "golang.org/x/oauth2/clientcredentials"
import (
"context"
"fmt"
"net/http"
"net/url"
"strings"
"golang.org/x/oauth2"
"golang.org/x/oauth2/internal"
)
// Config describes a 2-legged OAuth2 flow, with both the
// client application information and the server's endpoint URLs.
type Config struct {
// ClientID is the application's ID.
ClientID string
// ClientSecret is the application's secret.
ClientSecret string
// TokenURL is the resource server's token endpoint
// URL. This is a constant specific to each server.
TokenURL string
// Scope specifies optional requested permissions.
Scopes []string
// EndpointParams specifies additional parameters for requests to the token endpoint.
EndpointParams url.Values
// AuthStyle optionally specifies how the endpoint wants the
// client ID & client secret sent. The zero value means to
// auto-detect.
AuthStyle oauth2.AuthStyle
}
// Token uses client credentials to retrieve a token.
//
// The provided context optionally controls which HTTP client is used. See the oauth2.HTTPClient variable.
func (c *Config) Token(ctx context.Context) (*oauth2.Token, error) {
return c.TokenSource(ctx).Token()
}
// Client returns an HTTP client using the provided token.
// The token will auto-refresh as necessary.
//
// The provided context optionally controls which HTTP client
// is returned. See the oauth2.HTTPClient variable.
//
// The returned Client and its Transport should not be modified.
func (c *Config) Client(ctx context.Context) *http.Client {
return oauth2.NewClient(ctx, c.TokenSource(ctx))
}
// TokenSource returns a TokenSource that returns t until t expires,
// automatically refreshing it as necessary using the provided context and the
// client ID and client secret.
//
// Most users will use Config.Client instead.
func (c *Config) TokenSource(ctx context.Context) oauth2.TokenSource {
source := &tokenSource{
ctx: ctx,
conf: c,
}
return oauth2.ReuseTokenSource(nil, source)
}
type tokenSource struct {
ctx context.Context
conf *Config
}
// Token refreshes the token by using a new client credentials request.
// tokens received this way do not include a refresh token
func (c *tokenSource) Token() (*oauth2.Token, error) {
v := url.Values{
"grant_type": {"client_credentials"},
}
if len(c.conf.Scopes) > 0 {
v.Set("scope", strings.Join(c.conf.Scopes, " "))
}
for k, p := range c.conf.EndpointParams {
// Allow grant_type to be overridden to allow interoperability with
// non-compliant implementations.
if _, ok := v[k]; ok && k != "grant_type" {
return nil, fmt.Errorf("oauth2: cannot overwrite parameter %q", k)
}
v[k] = p
}
tk, err := internal.RetrieveToken(c.ctx, c.conf.ClientID, c.conf.ClientSecret, c.conf.TokenURL, v, internal.AuthStyle(c.conf.AuthStyle))
if err != nil {
if rErr, ok := err.(*internal.RetrieveError); ok {
return nil, (*oauth2.RetrieveError)(rErr)
}
return nil, err
}
t := &oauth2.Token{
AccessToken: tk.AccessToken,
TokenType: tk.TokenType,
RefreshToken: tk.RefreshToken,
Expiry: tk.Expiry,
}
return t.WithExtra(tk.Raw), nil
}

16
vendor/modules.txt vendored
View File

@ -13,6 +13,8 @@ cloud.google.com/go/internal/protostruct
cloud.google.com/go/internal/testutil
cloud.google.com/go/spanner/internal/backoff
cloud.google.com/go/compute/metadata
# code.cloudfoundry.org/gofileutils v0.0.0-20170111115228-4d0c80011a0f
code.cloudfoundry.org/gofileutils/fileutils
# contrib.go.opencensus.io/exporter/ocagent v0.4.12
contrib.go.opencensus.io/exporter/ocagent
# github.com/Azure/azure-sdk-for-go v27.1.0+incompatible
@ -40,6 +42,8 @@ github.com/Azure/go-autorest/autorest/azure/cli
github.com/DataDog/datadog-go/statsd
# github.com/Jeffail/gabs v1.1.1
github.com/Jeffail/gabs
# github.com/Masterminds/semver v1.4.2
github.com/Masterminds/semver
# github.com/Microsoft/go-winio v0.4.12
github.com/Microsoft/go-winio
# github.com/NYTimes/gziphandler v1.1.1
@ -168,6 +172,8 @@ github.com/circonus-labs/circonus-gometrics/checkmgr
github.com/circonus-labs/circonus-gometrics/api/config
# github.com/circonus-labs/circonusllhist v0.1.3
github.com/circonus-labs/circonusllhist
# github.com/cloudfoundry-community/go-cfclient v0.0.0-20190201205600-f136f9222381
github.com/cloudfoundry-community/go-cfclient
# github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c
github.com/cockroachdb/cockroach-go/crdb
# github.com/containerd/continuity v0.0.0-20181203112020-004b46473808
@ -273,7 +279,7 @@ github.com/hashicorp/errwrap
github.com/hashicorp/go-cleanhttp
# github.com/hashicorp/go-gcp-common v0.5.0
github.com/hashicorp/go-gcp-common/gcputil
# github.com/hashicorp/go-hclog v0.8.0
# github.com/hashicorp/go-hclog v0.9.2
github.com/hashicorp/go-hclog
# github.com/hashicorp/go-immutable-radix v1.0.0
github.com/hashicorp/go-immutable-radix
@ -329,6 +335,13 @@ github.com/hashicorp/vault-plugin-auth-gcp/plugin/cache
github.com/hashicorp/vault-plugin-auth-jwt
# github.com/hashicorp/vault-plugin-auth-kubernetes v0.5.1
github.com/hashicorp/vault-plugin-auth-kubernetes
# github.com/hashicorp/vault-plugin-auth-pcf v0.0.0-20190619165123-fb996be2877c
github.com/hashicorp/vault-plugin-auth-pcf
github.com/hashicorp/vault-plugin-auth-pcf/signatures
github.com/hashicorp/vault-plugin-auth-pcf/models
github.com/hashicorp/vault-plugin-auth-pcf/util
github.com/hashicorp/vault-plugin-auth-pcf/testing/certificates
github.com/hashicorp/vault-plugin-auth-pcf/testing/pcf
# github.com/hashicorp/vault-plugin-database-elasticsearch v0.0.0-20190508211750-4152192cdc0f
github.com/hashicorp/vault-plugin-database-elasticsearch
# github.com/hashicorp/vault-plugin-secrets-ad v0.5.1
@ -644,6 +657,7 @@ golang.org/x/oauth2/internal
golang.org/x/oauth2/google
golang.org/x/oauth2/jwt
golang.org/x/oauth2/jws
golang.org/x/oauth2/clientcredentials
# golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6
golang.org/x/sync/semaphore
# golang.org/x/sys v0.0.0-20190410170021-cc4d4f50624c