diff --git a/.dockerignore b/.dockerignore index 6e43c2a..ce32094 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,3 +2,6 @@ ./.dapper ./dist ./.trash-cache +./Dockerfile.dapper* +./external-lb.exe +./providers/zevenet/zevenet_test.go diff --git a/.gitignore b/.gitignore index 6c251e4..b7298e5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ external-lb /dist *.swp /.trash-cache +/Dockerfile.dapper* +/external-lb.exe diff --git a/main.go b/main.go index 141f06f..247155b 100644 --- a/main.go +++ b/main.go @@ -15,6 +15,8 @@ import ( _ "github.com/rancher/external-lb/providers/avi" _ "github.com/rancher/external-lb/providers/elbv1" _ "github.com/rancher/external-lb/providers/f5" + _ "github.com/rancher/external-lb/providers/gravitee" + _ "github.com/rancher/external-lb/providers/zevenet" ) const ( @@ -119,6 +121,19 @@ func main() { continue } + // filter regarding provider + if true { + metadataLBConfigsFiltered := make(map[string]model.LBConfig) + + for k, v := range metadataLBConfigs { + if pn, ok := v.LBLabels["provider"]; !ok || strings.EqualFold(pn, *providerName) { + metadataLBConfigsFiltered[k] = v + } + } + + metadataLBConfigs = metadataLBConfigsFiltered + } + logrus.Debugf("LB configs from metadata: %v", metadataLBConfigs) // A flapping service might cause the metadata version to change diff --git a/metadata/metadata.go b/metadata/metadata.go index cbadf14..1fb6ecf 100644 --- a/metadata/metadata.go +++ b/metadata/metadata.go @@ -2,15 +2,17 @@ package metadata import ( "fmt" + "strings" + "time" + "github.com/Sirupsen/logrus" "github.com/rancher/external-lb/model" "github.com/rancher/go-rancher-metadata/metadata" - "strings" - "time" ) const ( metadataURLTemplate = "http://%v/2015-12-19" + serviceLabelsPrefix = "io.rancher.service.external_lb." serviceLabelEndpoint = "io.rancher.service.external_lb.endpoint" serviceLabelEndpointLegacy = "io.rancher.service.external_lb_endpoint" @@ -109,7 +111,19 @@ func (m *MetadataClient) GetMetadataLBConfigs(targetPoolSuffix string) (map[stri continue } + // copy labels + labels := make(map[string]string) + + for key, value := range service.Labels { + if strings.HasPrefix(key, serviceLabelsPrefix) { + newKey := key[len(serviceLabelsPrefix):] + + labels[newKey] = value + } + } + lbConfig := model.LBConfig{} + lbConfig.LBLabels = labels lbConfig.LBEndpoint = endpoint lbConfig.LBTargetPort = portspec[0] lbConfig.LBTargetPoolName = fmt.Sprintf("%s_%s_%s_%s", service.Name, service.StackName, diff --git a/model/lb.go b/model/lb.go index 046410e..f4d8739 100644 --- a/model/lb.go +++ b/model/lb.go @@ -9,6 +9,9 @@ type LBConfig struct { LBTargetPoolName string LBTargetPort string LBTargets []LBTarget + + // LBLabels contains all Rancher labels starting with "io.rancher.service.external_lb" + LBLabels map[string]string } type LBTarget struct { diff --git a/providers/avi/aviutils.go b/providers/avi/aviutils.go index 0e95fa0..4da6902 100644 --- a/providers/avi/aviutils.go +++ b/providers/avi/aviutils.go @@ -128,7 +128,12 @@ func formLBConfig(vs map[string]interface{}, vsName := vs["name"].(string) poolName := pool["name"].(string) - return model.LBConfig{vsName, poolName, defaultPort, lbTargets} + return model.LBConfig{ + LBEndpoint: vsName, + LBTargetPoolName: poolName, + LBTargetPort: defaultPort, + LBTargets: lbTargets, + } } func GetVsFqdn(vs map[string]interface{}) (string, error) { diff --git a/providers/gravitee/README.md b/providers/gravitee/README.md new file mode 100644 index 0000000..209f3bd --- /dev/null +++ b/providers/gravitee/README.md @@ -0,0 +1,30 @@ +# Gravitee Management API Provider + +## Labels (on Rancher services) + +The following labels are supported on backend container, which host the services to be added to the Gravitee API. + +| Label Name | Description | Example | Optional | +|-----------|------|-------|-------| +| io.rancher.service.external_lb.endpoint | Gravitee API label to look for. | my-fancy-api | No +| io.rancher.service.external_lb.provider | Has to be set to 'gravitee' to be handled by LB provider. Ignored if not set. | gravitee | Yes +| io.rancher.service.external_lb.encrypt | The backend service port is an HTTPS endpoint and re-encryption is required. Default is false. | true | Yes +| io.rancher.service.external_lb.keep_alive | Requests to the backend service will use HTTP keep alive. Default is true. | false | Yes +| io.rancher.service.external_lb.pipelining | Requests to the backend service will be written to connections without waiting for previous responses to return. Default is false. | true | Yes +| io.rancher.service.external_lb.compress | Enables gzip compression support, and will be able to handle compressed response bodies. Default is true. | false | Yes +| io.rancher.service.external_lb.follow_redirects | Follows redirects returned by the backend service. Default is false. | true | Yes +| io.rancher.service.external_lb.conn_timeout | Time in milliseconds to wait for the backend service to accept the connection. Default is 5000. | 10000 | Yes +| io.rancher.service.external_lb.read_timeout | Time in milliseconds to wait for the backend service to start sending the response. Default is 10000. | 60000 | Yes +| io.rancher.service.external_lb.idle_timeout | Time in milliseconds to wait for an idle connection to be closed. Default is 60000. | 60000 | Yes +| io.rancher.service.external_lb.max_conn | Maximum amount of concurrent connections to a single backend service. Default is 100. | 5000 | Yes + +Important: The `io.rancher.service.external_lb.endpoint` has to exist to trigger the load-balancer integration. + +## Environment Variables + +| Variable Name | Description | Default Value | Optional | +|-----------|------|-------|------| +| GRAVITEE_HOST | The hostname of the Gravitee Management API, like *api.mygravitee.net* or *http://my-gravitee:8080* | | No | +| GRAVITEE_USER | The name of the user to user for Management API access. | | No | +| GRAVITEE_PWD | The password of the user to user for Management API access. | | No | +| LB_TARGET_RANCHER_SUFFIX | Service names in the farm will have this suffix. Choose something simple, like "rancher". | "rancher.internal" | Yes | diff --git a/providers/gravitee/gravitee.go b/providers/gravitee/gravitee.go new file mode 100644 index 0000000..2bfb0ac --- /dev/null +++ b/providers/gravitee/gravitee.go @@ -0,0 +1,409 @@ +package graviteelb + +import ( + "crypto/sha1" + "fmt" + "io" + "net/url" + "os" + "sort" + "strconv" + "strings" + + log "github.com/Sirupsen/logrus" + "github.com/konsorten/go-gravitee" + "github.com/rancher/external-lb/model" + "github.com/rancher/external-lb/providers" +) + +const ( + providerName = "Gravitee" + providerSlug = "gravitee" +) + +type GraviteeProvider struct { + client *gravitee.GraviteeSession + configCache map[string]string +} + +func init() { + providers.RegisterProvider(providerSlug, new(GraviteeProvider)) +} + +func getApiMetadataMap(md []gravitee.ApiMetadata) map[string]string { + r := make(map[string]string) + + for _, m := range md { + r[m.Key] = m.Value() + } + + return r +} + +func getConfigHash(config *model.LBConfig) string { + h := sha1.New() + io.WriteString(h, config.LBEndpoint) + io.WriteString(h, "###") + + labels := make([]string, 0) + + for k, _ := range config.LBLabels { + labels = append(labels, k) + } + + sort.Strings(labels) + + for _, k := range labels { + v := config.LBLabels[k] + + io.WriteString(h, k) + io.WriteString(h, ":::") + io.WriteString(h, v) + io.WriteString(h, ";;;") + } + + io.WriteString(h, "###") + io.WriteString(h, config.LBTargetPoolName) + io.WriteString(h, "###") + io.WriteString(h, config.LBTargetPort) + io.WriteString(h, "###") + + for _, v := range config.LBTargets { + io.WriteString(h, v.HostIP) + io.WriteString(h, ":") + io.WriteString(h, v.Port) + } + + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func (p *GraviteeProvider) Init() (err error) { + host := os.Getenv("GRAVITEE_HOST") + if len(host) == 0 { + return fmt.Errorf("GRAVITEE_HOST is not set") + } + + user := os.Getenv("GRAVITEE_USER") + if len(user) == 0 { + return fmt.Errorf("GRAVITEE_USER is not set") + } + + pwd := os.Getenv("GRAVITEE_PWD") + if len(pwd) == 0 { + return fmt.Errorf("GRAVITEE_PWD is not set") + } + + log.Debugf("Initializing Gravitee provider: %s, user: %s, pwd-length: %d", host, user, len(pwd)) + + p.client, err = gravitee.Connect(host, user, pwd, nil) + + if err != nil { + return + } + + p.configCache = make(map[string]string) + + log.Infof("Configured %s provider using %s", p.GetName(), host) + return +} + +func (p *GraviteeProvider) GetName() string { + return providerName +} + +func (p *GraviteeProvider) HealthCheck() error { + success, msg := p.client.Ping() + + if !success { + return fmt.Errorf("Failed to ping Gravitee loadbalancer: %v", msg) + } + + return nil +} + +func (p *GraviteeProvider) AddLBConfig(config model.LBConfig) (string, error) { + // first check if changes can be made + if available, msg := p.client.Ping(); !available { + return "", fmt.Errorf("Failed to ping Gravitee loadbalancer: %v", msg) + } + + apis, err := p.client.GetAPIsByLabel(config.LBEndpoint) + if err != nil { + log.Errorf("Failed to retrieve APIs with label %v: %v", config.LBEndpoint, err) + return "", nil + } + if apis == nil || len(apis) <= 0 { + log.Warnf("No APIs found with with label %v", config.LBEndpoint) + return "", nil + } + + // add configurations + for _, api := range apis { + // configure + _, err := p.addLBConfigSingle(api, config) + if err != nil { + log.Errorf("Failed to update API %v: %v", api, err) + } + } + + return "", nil +} + +func (p *GraviteeProvider) addLBConfigSingle(api gravitee.ApiInfo, config model.LBConfig) (string, error) { + // check if the config did change + configHash := getConfigHash(&config) + + if hash, ok := p.configCache[api.ID]; ok && hash == configHash { + log.Infof("Skipping config update of unchanged API %v", api) + return "", nil + } + + log.Debugf("Adding endpoints on API: %v", api) + log.Debugf("Service labels: %v", config.LBLabels) + + // parse labels + encryptedBackendsStr, _ := config.LBLabels["encrypt"] + encryptedBackends := encryptedBackendsStr == "true" + + keepAliveStr, _ := config.LBLabels["keep_alive"] + keepAlive := keepAliveStr == "true" || keepAliveStr == "" + + pipeliningStr, _ := config.LBLabels["pipelining"] + pipelining := pipeliningStr == "true" + + compressStr, _ := config.LBLabels["compress"] + compress := compressStr == "true" || compressStr == "" + + followRedirectsStr, _ := config.LBLabels["follow_redirects"] + followRedirects := followRedirectsStr == "true" + + connectTimeoutStr, _ := config.LBLabels["conn_timeout"] + readTimeoutStr, _ := config.LBLabels["read_timeout"] + idleTimeoutStr, _ := config.LBLabels["idle_timeout"] + maxConnectionsStr, _ := config.LBLabels["max_conn"] + + // update endpoints + endpoints := make([]gravitee.ApiDetailsEndpoint, 0) + + for _, ep := range config.LBTargets { + e := gravitee.MakeApiDetailsEndpoint(ep.HostIP, fmt.Sprintf("http://%v:%v", ep.HostIP, ep.Port)) + + e.Http.KeepAlive = keepAlive + e.Http.Pipelining = pipelining + e.Http.UseCompression = compress + e.Http.FollowRedirects = followRedirects + e.SSL.IsEnabled = encryptedBackends + + if connectTimeoutStr != "" { + e.Http.ConnectTimeoutMS, _ = strconv.Atoi(connectTimeoutStr) + } + if readTimeoutStr != "" { + e.Http.ReadTimeoutMS, _ = strconv.Atoi(readTimeoutStr) + } + if idleTimeoutStr != "" { + e.Http.IdleTimeoutMS, _ = strconv.Atoi(idleTimeoutStr) + } + if maxConnectionsStr != "" { + e.Http.MaxConcurrentConnections, _ = strconv.Atoi(maxConnectionsStr) + } + + endpoints = append(endpoints, e) + } + + err := p.client.AddOrUpdateEndpoints(api.ID, endpoints, true) + if err != nil { + return "", fmt.Errorf("Failed to update endpoints for API %v on Gravitee loadbalancer: %v", api, err) + } + + // add meta-data + p.client.SetLocalAPIMetadata(api.ID, "rancher-lb-pool-name", config.LBTargetPoolName, gravitee.ApiMetadataFormat_String) + p.client.SetLocalAPIMetadata(api.ID, "rancher-lb-port", config.LBTargetPort, gravitee.ApiMetadataFormat_String) + p.client.SetLocalAPIMetadata(api.ID, "rancher-lb-endpoint", config.LBEndpoint, gravitee.ApiMetadataFormat_String) + p.client.SetLocalAPIMetadata(api.ID, "rancher-lb-hash", configHash, gravitee.ApiMetadataFormat_String) + + // deploy new api config + log.Debugf("Deploying API: %v", api) + + err = p.client.DeployAPI(api.ID) + + if err != nil { + return "", fmt.Errorf("Failed to deploy API %v on Gravitee loadbalancer: %v", api, err) + } + + // update cache + p.configCache[api.ID] = configHash + + return "", nil +} + +func (p *GraviteeProvider) UpdateLBConfig(config model.LBConfig) (string, error) { + return p.AddLBConfig(config) +} + +func (p *GraviteeProvider) RemoveLBConfig(config model.LBConfig) error { + // first check if changes can be made + if available, msg := p.client.Ping(); !available { + return fmt.Errorf("Failed to ping Gravitee loadbalancer: %v", msg) + } + + apis, err := p.client.GetAPIsByLabel(config.LBEndpoint) + if err != nil { + log.Errorf("Failed to retrieve APIs with label %v: %v", config.LBEndpoint, err) + return nil + } + if apis == nil || len(apis) <= 0 { + log.Warnf("No APIs found with with label %v", config.LBEndpoint) + return nil + } + + // add configurations + for _, api := range apis { + // configure + err := p.removeLBConfigSingle(api, config) + if err != nil { + log.Errorf("Failed to update API %v: %v", api, err) + } + } + + return nil +} + +func (p *GraviteeProvider) removeLBConfigSingle(api gravitee.ApiInfo, config model.LBConfig) error { + // remove from cache + delete(p.configCache, api.ID) + + // clear all endpoints + endpoints := make([]gravitee.ApiDetailsEndpoint, 0) + + err := p.client.AddOrUpdateEndpoints(api.ID, endpoints, true) + if err != nil { + return fmt.Errorf("Failed to update endpoints for API %v on Gravitee loadbalancer: %v", api, err) + } + + // remove meta-data + meta, err := p.client.GetAPIMetadata(api.ID) + if err != nil { + return fmt.Errorf("Failed to retrieve metadata for API %v on Gravitee loadbalancer: %v", api, err) + } + + for _, m := range meta { + if m.IsLocal() && strings.HasPrefix(m.Key, "rancher-lb-") { + err := p.client.UnsetLocalAPIMetadata(api.ID, m.Key) + if err != nil { + log.Warnf("Failed to delete local metadata '%v' from API %v on Gravitee loadbalancer: %v", m.Key, api, err) + } + } + } + + // deploy new api config + log.Debugf("Deploying API: %v", api) + + err = p.client.DeployAPI(api.ID) + if err != nil { + return fmt.Errorf("Failed to deploy API %v on Gravitee loadbalancer: %v", api, err) + } + + return nil +} + +func (p *GraviteeProvider) GetLBConfigs() ([]model.LBConfig, error) { + // first check if changes can be made + if available, msg := p.client.Ping(); !available { + return nil, fmt.Errorf("Failed to ping Gravitee loadbalancer: %v", msg) + } + + // get all APIs + apis, err := p.client.GetAllAPIs() + if err != nil { + return nil, fmt.Errorf("Failed to get APIs from Gravitee loadbalancer: %v", err) + } + + lbConfigs := make([]model.LBConfig, 0) + + for _, apiInfo := range apis { + // check if the farm exists + log.Debugf("Gathering existing API: %v", apiInfo.Name) + + // get metadata (and check if this is a rancher API) + metaRaw, err := p.client.GetAPIMetadata(apiInfo.ID) + if err != nil { + return nil, fmt.Errorf("Failed to get API metadata for %v from Gravitee loadbalancer: %v", apiInfo, err) + } + + if true { + found := false + + for _, m := range metaRaw { + if m.IsLocal() && strings.HasPrefix(m.Key, "rancher-lb-") { + found = true + break + } + } + + if !found { + // ignore this api + continue + } + } + + meta := getApiMetadataMap(metaRaw) + + // retrieve details + api, err := p.client.GetAPI(apiInfo.ID) + if err != nil { + return nil, fmt.Errorf("Failed to get API %v from Gravitee loadbalancer: %v", apiInfo, err) + } + if api == nil { + return nil, fmt.Errorf("API not found on Gravitee loadbalancer: %v", apiInfo) + } + + // build config + cfg := model.LBConfig{} + ok := false + + if cfg.LBTargetPoolName, ok = meta["rancher-lb-pool-name"]; !ok { + log.Errorf("Failed to retrieve target pool name from 'rancher-lb-pool-name' API metadata for %v from Gravitee loadbalancer", apiInfo) + continue + } + + if cfg.LBTargetPort, ok = meta["rancher-lb-port"]; !ok { + log.Debugf("Failed to retrieve virtual port from 'rancher-lb-port' API metadata for %v from Gravitee loadbalancer; Assuming port HTTPS (443) ...", apiInfo) + + cfg.LBTargetPort = "443" + } + + if cfg.LBEndpoint, ok = meta["rancher-lb-endpoint"]; !ok { + log.Errorf("Failed to retrieve target endpoint from 'rancher-lb-endpoint' API metadata for %v from Gravitee loadbalancer", apiInfo) + continue + } + + if configHash, ok := meta["rancher-lb-hash"]; ok { + // add to cache, if new + if _, kk := p.configCache[api.ID]; !kk { + p.configCache[api.ID] = configHash + } + } + + // get endpoints + for _, ep := range api.Proxy.Endpoints { + log.Debugf("Found endpoint on API '%v': %v", api, ep.Name) + + epUrl, err := url.Parse(ep.Target) + if err != nil { + log.Warnf("Failed to parse target URL for %v endpoint on %v API from Gravitee loadbalancer: %v: %v", ep.Name, apiInfo, ep.Target, err) + continue + } + + cfg.LBTargets = append(cfg.LBTargets, model.LBTarget{ + HostIP: epUrl.Host, + Port: epUrl.Port(), + }) + } + + // done + lbConfigs = append(lbConfigs, cfg) + } + + // transform + return lbConfigs, nil +} diff --git a/providers/gravitee/gravitee_test.go b/providers/gravitee/gravitee_test.go new file mode 100644 index 0000000..9dd9e9c --- /dev/null +++ b/providers/gravitee/gravitee_test.go @@ -0,0 +1,37 @@ +package graviteelb + +import ( + "testing" + + "github.com/rancher/external-lb/model" +) + +func TestGetConfigHash(t *testing.T) { + config := model.LBConfig{ + LBEndpoint: "sadsdafdsaf", + LBLabels: map[string]string{ + "kdjvtziugdbfn": "ksljndbvfjhsdjhvbf", + "8475vh687thvg": "cb43t5634", + "vpornihzube6g7": "73n4ct67b348v", + "iu4bt367r5g34": "783v46t48b34", + }, + LBTargetPoolName: "sdjhfiu434", + LBTargetPort: "345", + LBTargets: []model.LBTarget{ + model.LBTarget{HostIP: "234.234.223.4", Port: "7645"}, + model.LBTarget{HostIP: "234.9.223.4", Port: "44"}, + }, + } + + hash := getConfigHash(&config) + + t.Logf("Hash: %v", hash) + + for i := 0; i < 10; i++ { + hash2 := getConfigHash(&config) + + if hash != hash2 { + t.Fatal("Config hash is different") + } + } +} diff --git a/providers/zevenet/README.md b/providers/zevenet/README.md new file mode 100644 index 0000000..7150f84 --- /dev/null +++ b/providers/zevenet/README.md @@ -0,0 +1,29 @@ +# Zevenet Loadbalancer Provider + +## Labels (on backend services) + +The following labels are supported on backend container, which host the services to be added to the loadbalancer. + +| Label Name | Description | Example | Optional | Farm-specific | +|-----------|------|-------|-------|---| +| io.rancher.service.external_lb.endpoint | Hostname pattern to use for the service. | blog(\\.example\\.com)? | No | Yes +| io.rancher.service.external_lb.provider | Has to be set to 'zevenet' to be handled by LB provider. Ignored if not set. | zevenet | Yes | No +| io.rancher.service.external_lb.farms | List of farms to add the service to. Separated by comma. | MainHTTP,MainHTTPS | No | No +| io.rancher.service.external_lb.http_redirect_url | Redirect URL to use for HTTP requests (without HTTPS). | https://blog.example.com | Yes | Yes +| io.rancher.service.external_lb.url_pattern | The URL pattern to use for limiting handled requests. | ^public/ | Yes | Yes +| io.rancher.service.external_lb.check | The Farm Guarian check command used to monitor the backend service.
If set to "true" the default "check_http [-S] -H HOST -p PORT" will be used. [1] | check_http -H HOST -p PORT | Yes | No +| io.rancher.service.external_lb.encrypt | The backend service port is an HTTPS endpoint and re-encryption is required. Default is false. | true | Yes | No + +Some labels are farm-specific and are support both as `io.rancher.service.external_lb.setting` and `io.rancher.service.external_lb.farm.setting`. The name of the farm must be lowercase. + +Important: The `io.rancher.service.external_lb.endpoint` has to exist to trigger the load-balancer integration. + + [1] see https://www.zevenet.com/knowledge-base/enterprise-edition/enterprise-edition-v5-0-administration-guide/lslb-farms-update-farm-guardian/ + +## Environment Variables + +| Variable Name | Description | Default Value | Optional | +|-----------|------|-------|------| +| ZAPI_HOST | The hostname of the Zevenet Loadbalancer, like *mylbcluster:444* | | No | +| ZAPI_KEY | The key of the *zapi* user. | | No | +| LB_TARGET_RANCHER_SUFFIX | Service names in the farm will have this suffix. Choose something simple, like "rancher". | "rancher.internal" | Yes | diff --git a/providers/zevenet/zevenet.go b/providers/zevenet/zevenet.go new file mode 100644 index 0000000..73db8f3 --- /dev/null +++ b/providers/zevenet/zevenet.go @@ -0,0 +1,457 @@ +package zevenet + +import ( + "crypto/sha1" + "fmt" + "io" + "os" + "sort" + "strconv" + "strings" + + log "github.com/Sirupsen/logrus" + zlb "github.com/konsorten/zevenet-lb-go" + "github.com/rancher/external-lb/model" + "github.com/rancher/external-lb/providers" +) + +const ( + providerName = "Zevenet" + providerSlug = "zevenet" +) + +type ZevenetProvider struct { + client *zlb.ZapiSession + configCache map[string]string +} + +func init() { + providers.RegisterProvider(providerSlug, new(ZevenetProvider)) +} + +func encodeServiceName(name string) string { + // replace invalid chars + name = strings.Replace(name, ".", "--D--", -1) + name = strings.Replace(name, "_", "--U--", -1) + + return name +} + +func decodeServiceName(name string) string { + // replace invalid chars + name = strings.Replace(name, "--D--", ".", -1) + name = strings.Replace(name, "--U--", "_", -1) + + return name +} + +func getServiceNameEx(config *model.LBConfig) (serviceName, envUuid, suffix string, err error) { + // format: __rancher.internal + parts := strings.Split(config.LBTargetPoolName, "_") + + if len(parts) < 3 { + err = fmt.Errorf("Failed to split service name '%v': %v", config.LBTargetPoolName, err) + return + } + + // done + serviceName = encodeServiceName(strings.Join(parts[:len(parts)-2], "_")) + envUuid = encodeServiceName(parts[len(parts)-2]) + suffix = encodeServiceName(parts[len(parts)-1]) + return +} + +func getServiceName(config *model.LBConfig) string { + // format: __rancher.internal + pn := config.LBTargetPoolName + + return encodeServiceName(pn) +} + +func getConfigHash(config *model.LBConfig) string { + h := sha1.New() + io.WriteString(h, config.LBEndpoint) + io.WriteString(h, "###") + + labels := make([]string, 0) + + for k, _ := range config.LBLabels { + labels = append(labels, k) + } + + sort.Strings(labels) + + for _, k := range labels { + v := config.LBLabels[k] + + io.WriteString(h, k) + io.WriteString(h, ":::") + io.WriteString(h, v) + io.WriteString(h, ";;;") + } + + io.WriteString(h, "###") + io.WriteString(h, config.LBTargetPoolName) + io.WriteString(h, "###") + io.WriteString(h, config.LBTargetPort) + io.WriteString(h, "###") + + for _, v := range config.LBTargets { + io.WriteString(h, v.HostIP) + io.WriteString(h, ":") + io.WriteString(h, v.Port) + } + + return fmt.Sprintf("%x", h.Sum(nil)) +} + +func getPoolName(service *zlb.ServiceDetails) string { + pn := service.ServiceName + + return decodeServiceName(pn) +} + +func (p *ZevenetProvider) Init() (err error) { + host := os.Getenv("ZAPI_HOST") + if len(host) == 0 { + return fmt.Errorf("ZAPI_HOST is not set") + } + + zapiKey := os.Getenv("ZAPI_KEY") + if len(zapiKey) == 0 { + return fmt.Errorf("ZAPI_KEY is not set") + } + + log.Debugf("Initializing Zevenet provider: %s, key-length: %d", host, len(zapiKey)) + + p.client, err = zlb.Connect(host, zapiKey, nil) + + if err != nil { + return + } + + p.configCache = make(map[string]string) + + log.Infof("Configured %s provider using %s", p.GetName(), host) + return +} + +func (p *ZevenetProvider) GetName() string { + return providerName +} + +func (p *ZevenetProvider) HealthCheck() error { + success, msg := p.client.Ping() + + if !success { + return fmt.Errorf("Failed to ping Zevenet loadbalancer: %v", msg) + } + + return nil +} + +func (p *ZevenetProvider) AddLBConfig(config model.LBConfig) (string, error) { + // first check if changes can be made + if available, msg := p.client.Ping(); !available { + return "", fmt.Errorf("Failed to ping Zevenet loadbalancer: %v", msg) + } + + // retrieve farm list + farmList, _ := config.LBLabels["farms"] + + if farmList == "" { + return "", fmt.Errorf("No farm specified; missing 'io.rancher.service.external_lb.farms' label?") + } + + farms := strings.Split(farmList, ",") + + // add configurations + for _, farmName := range farms { + // ignore empty entry + if farmName == "" { + continue + } + + // configure + _, err := p.addLBConfigSingleFarm(farmName, config) + + if err != nil { + log.Errorf("Failed to add farm %v: %v", farmName, err) + } + } + + return "", nil +} + +func (p *ZevenetProvider) addLBConfigSingleFarm(farmName string, config model.LBConfig) (string, error) { + // check if the config did change + serviceName := getServiceName(&config) + configHash := getConfigHash(&config) + cacheEntryName := fmt.Sprintf("%v#%v", farmName, serviceName) + + if hash, ok := p.configCache[cacheEntryName]; ok && hash == configHash { + log.Infof("Skipping config update of unchanged service %v on farm %v", serviceName, farmName) + return "", nil + } + + // check if the farm exists + farm, err := p.client.GetFarm(farmName) + + if err != nil { + return "", fmt.Errorf("Failed to get farm from Zevenet loadbalancer: %v", err) + } + + if farm == nil { + return "", fmt.Errorf("Farm not found on Zevenet loadbalancer: %v", farmName) + } + + // delete the service + // (the environment id can change, so ignore it) + if true { + sn, _, suffix, err := getServiceNameEx(&config) + + if err != nil { + return "", fmt.Errorf("Failed to get service name: %v", err) + } + + for _, srv := range farm.Services { + if strings.HasPrefix(srv.ServiceName, sn+"--U--") && strings.HasSuffix(srv.ServiceName, "--U--"+suffix) { + log.Debugf("Deleting service on farm %v: %v", farm.FarmName, srv.ServiceName) + + _, err := p.client.DeleteService(srv.FarmName, srv.ServiceName) + + if err != nil { + return "", fmt.Errorf("Failed to delete service on Zevenet loadbalancer: %v", err) + } + } + } + } + + // check if http redirection applies + log.Debugf("Adding service on farm %v: %v", farm.FarmName, serviceName) + log.Debugf("Service labels: %v", config.LBLabels) + + httpRedirectURL, _ := config.LBLabels[fmt.Sprintf("%v.http_redirect_url", strings.ToLower(farm.FarmName))] + + if httpRedirectURL == "" { + httpRedirectURL, _ = config.LBLabels["http_redirect_url"] + } + + if farm.Listener != zlb.FarmListener_HTTP { + httpRedirectURL = "" + } + + hostPattern, _ := config.LBLabels[fmt.Sprintf("%v.endpoint", strings.ToLower(farm.FarmName))] + + if hostPattern == "" { + hostPattern = config.LBEndpoint + } + + urlPattern, _ := config.LBLabels[fmt.Sprintf("%v.url_pattern", strings.ToLower(farm.FarmName))] + + if urlPattern == "" { + urlPattern, _ = config.LBLabels["url_pattern"] + } + + encryptedBackendsStr, _ := config.LBLabels["encrypt"] + encryptedBackends := encryptedBackendsStr == "true" + + checkCmd, _ := config.LBLabels["check"] + + // re-create the service + service, err := p.client.CreateService(farm.FarmName, serviceName) + + if err != nil { + return "", fmt.Errorf("Failed to create service on Zevenet loadbalancer: %v", err) + } + + // update values + service.HostPattern = hostPattern + "$" + service.URLPattern = urlPattern + + if httpRedirectURL != "" { + log.Debugf("Setting redirect URL for service '%v': %v", serviceName, httpRedirectURL) + + service.RedirectURL = httpRedirectURL + service.RedirectType = zlb.ServiceRedirectType_Default + } else { + // enable farm guardian + if checkCmd != "" { + service.FarmGuardianEnabled = true + service.FarmGuardianLogsEnabled = zlb.OptionalBool_True + service.FarmGuardianCheckIntervalSeconds = 5 + + if checkCmd == "true" { + if encryptedBackends { + service.FarmGuardianScript = "check_http -S -H HOST -p PORT" + } else { + service.FarmGuardianScript = "check_http -H HOST -p PORT" + } + } else { + service.FarmGuardianScript = checkCmd + } + } + + // enable re-encryption + service.EncryptedBackends = encryptedBackends + } + + err = p.client.UpdateService(service) + + if err != nil { + return "", fmt.Errorf("Failed to update service on Zevenet loadbalancer: %v", err) + } + + // add backends (if not redirecting) + if httpRedirectURL == "" { + log.Debugf("Adding backends to service: %v", serviceName) + + for _, target := range config.LBTargets { + // get the port number + port, err := strconv.Atoi(target.Port) + + if err != nil { + return "", fmt.Errorf("Failed to parse port number '%v': %v", target.Port, err) + } + + // create the backend + log.Debugf("Adding backend to service '%v': %v:%v", serviceName, target.HostIP, port) + + _, err = p.client.CreateBackend(farm.FarmName, service.ServiceName, target.HostIP, port) + + if err != nil { + return "", fmt.Errorf("Failed to create backend on Zevenet loadbalancer: %v", err) + } + } + } + + // restart loadbalancer + log.Debugf("Restarting farm: %v", farm.FarmName) + + err = p.client.RestartFarm(farm.FarmName) + + if err != nil { + return "", fmt.Errorf("Failed to restart farm on Zevenet loadbalancer: %v", err) + } + + // update cache + p.configCache[cacheEntryName] = configHash + + return "", nil +} + +func (p *ZevenetProvider) UpdateLBConfig(config model.LBConfig) (string, error) { + return p.AddLBConfig(config) +} + +func (p *ZevenetProvider) RemoveLBConfig(config model.LBConfig) error { + // first check if changes can be made + if available, msg := p.client.Ping(); !available { + return fmt.Errorf("Failed to ping Zevenet loadbalancer: %v", msg) + } + + // retrieve farm list + farmList, _ := config.LBLabels["farms"] + + if farmList == "" { + return fmt.Errorf("No farm specified; missing 'io.rancher.service.external_lb.farms' label?") + } + + farms := strings.Split(farmList, ",") + serviceName := getServiceName(&config) + + for _, farmName := range farms { + // delete the service + log.Debugf("Deleting service on farm %v: %v", farmName, serviceName) + + deleted, err := p.client.DeleteService(farmName, serviceName) + + if err != nil { + return fmt.Errorf("Failed to delete service from Zevenet loadbalancer: %v", err) + } + + if !deleted { + // nothing deleted, skip restart + log.Debugf("Service does not exist on farm %v: %v; Skipping farm restart...", farmName, serviceName) + + return nil + } + + // restart loadbalancer + log.Debugf("Restarting farm: %v", farmName) + + err = p.client.RestartFarm(farmName) + + if err != nil { + return fmt.Errorf("Failed to restart farm on Zevenet loadbalancer: %v", err) + } + + // remove from cache + cacheEntryName := fmt.Sprintf("%v#%v", farmName, serviceName) + + delete(p.configCache, cacheEntryName) + } + + return nil +} + +func (p *ZevenetProvider) GetLBConfigs() ([]model.LBConfig, error) { + // first check if changes can be made + if available, msg := p.client.Ping(); !available { + return nil, fmt.Errorf("Failed to ping Zevenet loadbalancer: %v", msg) + } + + // get all farms + farms, err := p.client.GetAllFarms() + + if err != nil { + return nil, fmt.Errorf("Failed to get farms from Zevenet loadbalancer: %v", err) + } + + lbConfigMap := make(map[string]model.LBConfig) + + for _, farmInfo := range farms { + // check if the farm exists + log.Debugf("Gathering existing services on farm: %v", farmInfo.FarmName) + + farm, err := p.client.GetFarm(farmInfo.FarmName) + + if err != nil { + return nil, fmt.Errorf("Failed to get farm %v from Zevenet loadbalancer: %v", farmInfo.FarmName, err) + } + + if farm == nil { + return nil, fmt.Errorf("Farm not found on Zevenet loadbalancer: %v", farmInfo.FarmName) + } + + // get services + for _, service := range farm.Services { + log.Debugf("Found service on farm '%v': %v", farm.FarmName, service.ServiceName) + + cfg := model.LBConfig{ + LBTargetPoolName: getPoolName(&service), + LBTargetPort: strconv.Itoa(farm.VirtualPort), + LBEndpoint: service.HostPattern, + } + + // get backends + for _, backend := range service.Backends { + cfg.LBTargets = append(cfg.LBTargets, model.LBTarget{ + HostIP: backend.IPAddress, + Port: strconv.Itoa(backend.Port), + }) + } + + lbConfigMap[service.ServiceName] = cfg + } + } + + // transform + lbConfigs := make([]model.LBConfig, len(lbConfigMap)) + + for _, cfg := range lbConfigMap { + lbConfigs = append(lbConfigs, cfg) + } + + return lbConfigs, nil +} diff --git a/providers/zevenet/zevenet_test.go b/providers/zevenet/zevenet_test.go new file mode 100644 index 0000000..147411a --- /dev/null +++ b/providers/zevenet/zevenet_test.go @@ -0,0 +1,78 @@ +package zevenet + +import ( + "testing" + + "github.com/rancher/external-lb/model" +) + +func TestEncodeServiceName(t *testing.T) { + const orig = "consul_consul_7734d6f0-0079-415d-984b-a827238c2427_rancher" + + c1 := encodeServiceName(orig) + c2 := decodeServiceName(c1) + + if c2 != orig { + t.Fatal("Re-encode failed") + } +} + +func TestGetServiceName(t *testing.T) { + const expect = "consul--U--consul--U--7734d6f0-0079-415d-984b-a827238c2427--U--rancher" + + n := getServiceName(&model.LBConfig{LBTargetPoolName: "consul_consul_7734d6f0-0079-415d-984b-a827238c2427_rancher"}) + + if n != expect { + t.Fatal("Failed to extract service name") + } +} + +func TestGetServiceNameEx(t *testing.T) { + sn, env, suffix, err := getServiceNameEx(&model.LBConfig{LBTargetPoolName: "consul_consul_7734d6f0-0079-415d-984b-a827238c2427_rancher"}) + + if err != nil { + t.Fatal(err) + } + + if sn != "consul--U--consul" { + t.Fatal("Failed to extract service name") + } + + if env != "7734d6f0-0079-415d-984b-a827238c2427" { + t.Fatal("Failed to extract environment UUID") + } + + if suffix != "rancher" { + t.Fatal("Failed to extract suffix") + } +} + +func TestGetConfigHash(t *testing.T) { + config := model.LBConfig{ + LBEndpoint: "sadsdafdsaf", + LBLabels: map[string]string{ + "kdjvtziugdbfn": "ksljndbvfjhsdjhvbf", + "8475vh687thvg": "cb43t5634", + "vpornihzube6g7": "73n4ct67b348v", + "iu4bt367r5g34": "783v46t48b34", + }, + LBTargetPoolName: "sdjhfiu434", + LBTargetPort: "345", + LBTargets: []model.LBTarget{ + model.LBTarget{HostIP: "234.234.223.4", Port: "7645"}, + model.LBTarget{HostIP: "234.9.223.4", Port: "44"}, + }, + } + + hash := getConfigHash(&config) + + t.Logf("Hash: %v", hash) + + for i := 0; i < 10; i++ { + hash2 := getConfigHash(&config) + + if hash != hash2 { + t.Fatal("Config hash is different") + } + } +} diff --git a/vendor/github.com/Jeffail/gabs/LICENSE b/vendor/github.com/Jeffail/gabs/LICENSE new file mode 100644 index 0000000..99a62c6 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2014 Ashley Jeffs + +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. diff --git a/vendor/github.com/Jeffail/gabs/README.md b/vendor/github.com/Jeffail/gabs/README.md new file mode 100644 index 0000000..044e9af --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/README.md @@ -0,0 +1,307 @@ +![Gabs](gabs_logo.png "Gabs") + +Gabs is a small utility for dealing with dynamic or unknown JSON structures in golang. It's pretty much just a helpful wrapper around the golang json.Marshal/json.Unmarshal behaviour and map[string]interface{} objects. It does nothing spectacular except for being fabulous. + +https://godoc.org/github.com/Jeffail/gabs + +## How to install: + +```bash +go get github.com/Jeffail/gabs +``` + +## How to use + +### Parsing and searching JSON + +```go +... + +import "github.com/Jeffail/gabs" + +jsonParsed, err := gabs.ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value1":10, + "value2":22 + }, + "alsoInner":{ + "value1":20 + } + } +}`)) + +var value float64 +var ok bool + +value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64) +// value == 10.0, ok == true + +value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64) +// value == 10.0, ok == true + +value, ok = jsonParsed.Path("does.not.exist").Data().(float64) +// value == 0.0, ok == false + +exists := jsonParsed.Exists("outter", "inner", "value1") +// exists == true + +exists := jsonParsed.Exists("does", "not", "exist") +// exists == false + +exists := jsonParsed.ExistsP("does.not.exist") +// exists == false + +... +``` + +### Iterating objects + +```go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"object":{ "first": 1, "second": 2, "third": 3 }}`)) + +// S is shorthand for Search +children, _ := jsonParsed.S("object").ChildrenMap() +for key, child := range children { + fmt.Printf("key: %v, value: %v\n", key, child.Data().(string)) +} + +... +``` + +### Iterating arrays + +```go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`)) + +// S is shorthand for Search +children, _ := jsonParsed.S("array").Children() +for _, child := range children { + fmt.Println(child.Data().(string)) +} + +... +``` + +Will print: + +``` +first +second +third +``` + +Children() will return all children of an array in order. This also works on objects, however, the children will be returned in a random order. + +### Searching through arrays + +If your JSON structure contains arrays you can still search the fields of the objects within the array, this returns a JSON array containing the results for each element. + +```go +... + +jsonParsed, _ := gabs.ParseJSON([]byte(`{"array":[ {"value":1}, {"value":2}, {"value":3} ]}`)) +fmt.Println(jsonParsed.Path("array.value").String()) + +... +``` + +Will print: + +``` +[1,2,3] +``` + +### Generating JSON + +```go +... + +jsonObj := gabs.New() +// or gabs.Consume(jsonObject) to work on an existing map[string]interface{} + +jsonObj.Set(10, "outter", "inner", "value") +jsonObj.SetP(20, "outter.inner.value2") +jsonObj.Set(30, "outter", "inner2", "value3") + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}} +``` + +To pretty-print: + +```go +... + +fmt.Println(jsonObj.StringIndent("", " ")) + +... +``` + +Will print: + +``` +{ + "outter": { + "inner": { + "value": 10, + "value2": 20 + }, + "inner2": { + "value3": 30 + } + } +} +``` + +### Generating Arrays + +```go +... + +jsonObj := gabs.New() + +jsonObj.Array("foo", "array") +// Or .ArrayP("foo.array") + +jsonObj.ArrayAppend(10, "foo", "array") +jsonObj.ArrayAppend(20, "foo", "array") +jsonObj.ArrayAppend(30, "foo", "array") + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"foo":{"array":[10,20,30]}} +``` + +Working with arrays by index: + +```go +... + +jsonObj := gabs.New() + +// Create an array with the length of 3 +jsonObj.ArrayOfSize(3, "foo") + +jsonObj.S("foo").SetIndex("test1", 0) +jsonObj.S("foo").SetIndex("test2", 1) + +// Create an embedded array with the length of 3 +jsonObj.S("foo").ArrayOfSizeI(3, 2) + +jsonObj.S("foo").Index(2).SetIndex(1, 0) +jsonObj.S("foo").Index(2).SetIndex(2, 1) +jsonObj.S("foo").Index(2).SetIndex(3, 2) + +fmt.Println(jsonObj.String()) + +... +``` + +Will print: + +``` +{"foo":["test1","test2",[1,2,3]]} +``` + +### Converting back to JSON + +This is the easiest part: + +```go +... + +jsonParsedObj, _ := gabs.ParseJSON([]byte(`{ + "outter":{ + "values":{ + "first":10, + "second":11 + } + }, + "outter2":"hello world" +}`)) + +jsonOutput := jsonParsedObj.String() +// Becomes `{"outter":{"values":{"first":10,"second":11}},"outter2":"hello world"}` + +... +``` + +And to serialize a specific segment is as simple as: + +```go +... + +jsonParsedObj := gabs.ParseJSON([]byte(`{ + "outter":{ + "values":{ + "first":10, + "second":11 + } + }, + "outter2":"hello world" +}`)) + +jsonOutput := jsonParsedObj.Search("outter").String() +// Becomes `{"values":{"first":10,"second":11}}` + +... +``` + +### Merge two containers + +You can merge a JSON structure into an existing one, where collisions will be +converted into a JSON array. + +```go +jsonParsed1, _ := ParseJSON([]byte(`{"outter": {"value1": "one"}}`)) +jsonParsed2, _ := ParseJSON([]byte(`{"outter": {"inner": {"value3": "three"}}, "outter2": {"value2": "two"}}`)) + +jsonParsed1.Merge(jsonParsed2) +// Becomes `{"outter":{"inner":{"value3":"three"},"value1":"one"},"outter2":{"value2":"two"}}` +``` + +Arrays are merged: + +```go +jsonParsed1, _ := ParseJSON([]byte(`{"array": ["one"]}`)) +jsonParsed2, _ := ParseJSON([]byte(`{"array": ["two"]}`)) + +jsonParsed1.Merge(jsonParsed2) +// Becomes `{"array":["one", "two"]}` +``` + +### Parsing Numbers + +Gabs uses the `json` package under the bonnet, which by default will parse all number values into `float64`. If you need to parse `Int` values then you should use a `json.Decoder` (https://golang.org/pkg/encoding/json/#Decoder): + +```go +sample := []byte(`{"test":{"int":10, "float":6.66}}`) +dec := json.NewDecoder(bytes.NewReader(sample)) +dec.UseNumber() + +val, err := gabs.ParseJSONDecoder(dec) +if err != nil { + t.Errorf("Failed to parse: %v", err) + return +} + +intValue, err := val.Path("test.int").Data().(json.Number).Int64() +``` diff --git a/vendor/github.com/Jeffail/gabs/gabs.go b/vendor/github.com/Jeffail/gabs/gabs.go new file mode 100644 index 0000000..a27a711 --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/gabs.go @@ -0,0 +1,579 @@ +/* +Copyright (c) 2014 Ashley Jeffs + +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. +*/ + +// Package gabs implements a simplified wrapper around creating and parsing JSON. +package gabs + +import ( + "bytes" + "encoding/json" + "errors" + "io" + "io/ioutil" + "strings" +) + +//-------------------------------------------------------------------------------------------------- + +var ( + // ErrOutOfBounds - Index out of bounds. + ErrOutOfBounds = errors.New("out of bounds") + + // ErrNotObjOrArray - The target is not an object or array type. + ErrNotObjOrArray = errors.New("not an object or array") + + // ErrNotObj - The target is not an object type. + ErrNotObj = errors.New("not an object") + + // ErrNotArray - The target is not an array type. + ErrNotArray = errors.New("not an array") + + // ErrPathCollision - Creating a path failed because an element collided with an existing value. + ErrPathCollision = errors.New("encountered value collision whilst building path") + + // ErrInvalidInputObj - The input value was not a map[string]interface{}. + ErrInvalidInputObj = errors.New("invalid input object") + + // ErrInvalidInputText - The input data could not be parsed. + ErrInvalidInputText = errors.New("input text could not be parsed") + + // ErrInvalidPath - The filepath was not valid. + ErrInvalidPath = errors.New("invalid file path") + + // ErrInvalidBuffer - The input buffer contained an invalid JSON string + ErrInvalidBuffer = errors.New("input buffer contained invalid JSON") +) + +//-------------------------------------------------------------------------------------------------- + +// Container - an internal structure that holds a reference to the core interface map of the parsed +// json. Use this container to move context. +type Container struct { + object interface{} +} + +// Data - Return the contained data as an interface{}. +func (g *Container) Data() interface{} { + if g == nil { + return nil + } + return g.object +} + +//-------------------------------------------------------------------------------------------------- + +// Path - Search for a value using dot notation. +func (g *Container) Path(path string) *Container { + return g.Search(strings.Split(path, ".")...) +} + +// Search - Attempt to find and return an object within the JSON structure by specifying the +// hierarchy of field names to locate the target. If the search encounters an array and has not +// reached the end target then it will iterate each object of the array for the target and return +// all of the results in a JSON array. +func (g *Container) Search(hierarchy ...string) *Container { + var object interface{} + + object = g.Data() + for target := 0; target < len(hierarchy); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + object, ok = mmap[hierarchy[target]] + if !ok { + return nil + } + } else if marray, ok := object.([]interface{}); ok { + tmpArray := []interface{}{} + for _, val := range marray { + tmpGabs := &Container{val} + res := tmpGabs.Search(hierarchy[target:]...) + if res != nil { + tmpArray = append(tmpArray, res.Data()) + } + } + if len(tmpArray) == 0 { + return nil + } + return &Container{tmpArray} + } else { + return nil + } + } + return &Container{object} +} + +// S - Shorthand method, does the same thing as Search. +func (g *Container) S(hierarchy ...string) *Container { + return g.Search(hierarchy...) +} + +// Exists - Checks whether a path exists. +func (g *Container) Exists(hierarchy ...string) bool { + return g.Search(hierarchy...) != nil +} + +// ExistsP - Checks whether a dot notation path exists. +func (g *Container) ExistsP(path string) bool { + return g.Exists(strings.Split(path, ".")...) +} + +// Index - Attempt to find and return an object within a JSON array by index. +func (g *Container) Index(index int) *Container { + if array, ok := g.Data().([]interface{}); ok { + if index >= len(array) { + return &Container{nil} + } + return &Container{array[index]} + } + return &Container{nil} +} + +// Children - Return a slice of all the children of the array. This also works for objects, however, +// the children returned for an object will NOT be in order and you lose the names of the returned +// objects this way. +func (g *Container) Children() ([]*Container, error) { + if array, ok := g.Data().([]interface{}); ok { + children := make([]*Container, len(array)) + for i := 0; i < len(array); i++ { + children[i] = &Container{array[i]} + } + return children, nil + } + if mmap, ok := g.Data().(map[string]interface{}); ok { + children := []*Container{} + for _, obj := range mmap { + children = append(children, &Container{obj}) + } + return children, nil + } + return nil, ErrNotObjOrArray +} + +// ChildrenMap - Return a map of all the children of an object. +func (g *Container) ChildrenMap() (map[string]*Container, error) { + if mmap, ok := g.Data().(map[string]interface{}); ok { + children := map[string]*Container{} + for name, obj := range mmap { + children[name] = &Container{obj} + } + return children, nil + } + return nil, ErrNotObj +} + +//-------------------------------------------------------------------------------------------------- + +// Set - Set the value of a field at a JSON path, any parts of the path that do not exist will be +// constructed, and if a collision occurs with a non object type whilst iterating the path an error +// is returned. +func (g *Container) Set(value interface{}, path ...string) (*Container, error) { + if len(path) == 0 { + g.object = value + return g, nil + } + var object interface{} + if g.object == nil { + g.object = map[string]interface{}{} + } + object = g.object + for target := 0; target < len(path); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + if target == len(path)-1 { + mmap[path[target]] = value + } else if mmap[path[target]] == nil { + mmap[path[target]] = map[string]interface{}{} + } + object = mmap[path[target]] + } else { + return &Container{nil}, ErrPathCollision + } + } + return &Container{object}, nil +} + +// SetP - Does the same as Set, but using a dot notation JSON path. +func (g *Container) SetP(value interface{}, path string) (*Container, error) { + return g.Set(value, strings.Split(path, ".")...) +} + +// SetIndex - Set a value of an array element based on the index. +func (g *Container) SetIndex(value interface{}, index int) (*Container, error) { + if array, ok := g.Data().([]interface{}); ok { + if index >= len(array) { + return &Container{nil}, ErrOutOfBounds + } + array[index] = value + return &Container{array[index]}, nil + } + return &Container{nil}, ErrNotArray +} + +// Object - Create a new JSON object at a path. Returns an error if the path contains a collision +// with a non object type. +func (g *Container) Object(path ...string) (*Container, error) { + return g.Set(map[string]interface{}{}, path...) +} + +// ObjectP - Does the same as Object, but using a dot notation JSON path. +func (g *Container) ObjectP(path string) (*Container, error) { + return g.Object(strings.Split(path, ".")...) +} + +// ObjectI - Create a new JSON object at an array index. Returns an error if the object is not an +// array or the index is out of bounds. +func (g *Container) ObjectI(index int) (*Container, error) { + return g.SetIndex(map[string]interface{}{}, index) +} + +// Array - Create a new JSON array at a path. Returns an error if the path contains a collision with +// a non object type. +func (g *Container) Array(path ...string) (*Container, error) { + return g.Set([]interface{}{}, path...) +} + +// ArrayP - Does the same as Array, but using a dot notation JSON path. +func (g *Container) ArrayP(path string) (*Container, error) { + return g.Array(strings.Split(path, ".")...) +} + +// ArrayI - Create a new JSON array at an array index. Returns an error if the object is not an +// array or the index is out of bounds. +func (g *Container) ArrayI(index int) (*Container, error) { + return g.SetIndex([]interface{}{}, index) +} + +// ArrayOfSize - Create a new JSON array of a particular size at a path. Returns an error if the +// path contains a collision with a non object type. +func (g *Container) ArrayOfSize(size int, path ...string) (*Container, error) { + a := make([]interface{}, size) + return g.Set(a, path...) +} + +// ArrayOfSizeP - Does the same as ArrayOfSize, but using a dot notation JSON path. +func (g *Container) ArrayOfSizeP(size int, path string) (*Container, error) { + return g.ArrayOfSize(size, strings.Split(path, ".")...) +} + +// ArrayOfSizeI - Create a new JSON array of a particular size at an array index. Returns an error +// if the object is not an array or the index is out of bounds. +func (g *Container) ArrayOfSizeI(size, index int) (*Container, error) { + a := make([]interface{}, size) + return g.SetIndex(a, index) +} + +// Delete - Delete an element at a JSON path, an error is returned if the element does not exist. +func (g *Container) Delete(path ...string) error { + var object interface{} + + if g.object == nil { + return ErrNotObj + } + object = g.object + for target := 0; target < len(path); target++ { + if mmap, ok := object.(map[string]interface{}); ok { + if target == len(path)-1 { + if _, ok := mmap[path[target]]; ok { + delete(mmap, path[target]) + } else { + return ErrNotObj + } + } + object = mmap[path[target]] + } else { + return ErrNotObj + } + } + return nil +} + +// DeleteP - Does the same as Delete, but using a dot notation JSON path. +func (g *Container) DeleteP(path string) error { + return g.Delete(strings.Split(path, ".")...) +} + +// Merge - Merges two gabs-containers +func (g *Container) Merge(toMerge *Container) error { + var recursiveFnc func(map[string]interface{}, []string) error + recursiveFnc = func(mmap map[string]interface{}, path []string) error { + for key, value := range mmap { + newPath := append(path, key) + if g.Exists(newPath...) { + target := g.Search(newPath...) + switch t := value.(type) { + case map[string]interface{}: + switch targetV := target.Data().(type) { + case map[string]interface{}: + if err := recursiveFnc(t, newPath); err != nil { + return err + } + case []interface{}: + g.Set(append(targetV, t), newPath...) + default: + newSlice := append([]interface{}{}, targetV) + g.Set(append(newSlice, t), newPath...) + } + case []interface{}: + for _, valueOfSlice := range t { + if err := g.ArrayAppend(valueOfSlice, newPath...); err != nil { + return err + } + } + default: + switch targetV := target.Data().(type) { + case []interface{}: + g.Set(append(targetV, t), newPath...) + default: + newSlice := append([]interface{}{}, targetV) + g.Set(append(newSlice, t), newPath...) + } + } + } else { + // path doesn't exist. So set the value + if _, err := g.Set(value, newPath...); err != nil { + return err + } + } + } + return nil + } + if mmap, ok := toMerge.Data().(map[string]interface{}); ok { + return recursiveFnc(mmap, []string{}) + } + return nil +} + +//-------------------------------------------------------------------------------------------------- + +/* +Array modification/search - Keeping these options simple right now, no need for anything more +complicated since you can just cast to []interface{}, modify and then reassign with Set. +*/ + +// ArrayAppend - Append a value onto a JSON array. If the target is not a JSON array then it will be +// converted into one, with its contents as the first element of the array. +func (g *Container) ArrayAppend(value interface{}, path ...string) error { + if array, ok := g.Search(path...).Data().([]interface{}); ok { + array = append(array, value) + _, err := g.Set(array, path...) + return err + } + + newArray := []interface{}{} + newArray = append(newArray, g.Search(path...).Data()) + newArray = append(newArray, value) + + _, err := g.Set(newArray, path...) + return err +} + +// ArrayAppendP - Append a value onto a JSON array using a dot notation JSON path. +func (g *Container) ArrayAppendP(value interface{}, path string) error { + return g.ArrayAppend(value, strings.Split(path, ".")...) +} + +// ArrayRemove - Remove an element from a JSON array. +func (g *Container) ArrayRemove(index int, path ...string) error { + if index < 0 { + return ErrOutOfBounds + } + array, ok := g.Search(path...).Data().([]interface{}) + if !ok { + return ErrNotArray + } + if index < len(array) { + array = append(array[:index], array[index+1:]...) + } else { + return ErrOutOfBounds + } + _, err := g.Set(array, path...) + return err +} + +// ArrayRemoveP - Remove an element from a JSON array using a dot notation JSON path. +func (g *Container) ArrayRemoveP(index int, path string) error { + return g.ArrayRemove(index, strings.Split(path, ".")...) +} + +// ArrayElement - Access an element from a JSON array. +func (g *Container) ArrayElement(index int, path ...string) (*Container, error) { + if index < 0 { + return &Container{nil}, ErrOutOfBounds + } + array, ok := g.Search(path...).Data().([]interface{}) + if !ok { + return &Container{nil}, ErrNotArray + } + if index < len(array) { + return &Container{array[index]}, nil + } + return &Container{nil}, ErrOutOfBounds +} + +// ArrayElementP - Access an element from a JSON array using a dot notation JSON path. +func (g *Container) ArrayElementP(index int, path string) (*Container, error) { + return g.ArrayElement(index, strings.Split(path, ".")...) +} + +// ArrayCount - Count the number of elements in a JSON array. +func (g *Container) ArrayCount(path ...string) (int, error) { + if array, ok := g.Search(path...).Data().([]interface{}); ok { + return len(array), nil + } + return 0, ErrNotArray +} + +// ArrayCountP - Count the number of elements in a JSON array using a dot notation JSON path. +func (g *Container) ArrayCountP(path string) (int, error) { + return g.ArrayCount(strings.Split(path, ".")...) +} + +//-------------------------------------------------------------------------------------------------- + +// Bytes - Converts the contained object back to a JSON []byte blob. +func (g *Container) Bytes() []byte { + if g.Data() != nil { + if bytes, err := json.Marshal(g.object); err == nil { + return bytes + } + } + return []byte("{}") +} + +// BytesIndent - Converts the contained object to a JSON []byte blob formatted with prefix, indent. +func (g *Container) BytesIndent(prefix string, indent string) []byte { + if g.object != nil { + if bytes, err := json.MarshalIndent(g.object, prefix, indent); err == nil { + return bytes + } + } + return []byte("{}") +} + +// String - Converts the contained object to a JSON formatted string. +func (g *Container) String() string { + return string(g.Bytes()) +} + +// StringIndent - Converts the contained object back to a JSON formatted string with prefix, indent. +func (g *Container) StringIndent(prefix string, indent string) string { + return string(g.BytesIndent(prefix, indent)) +} + +// EncodeOpt is a functional option for the EncodeJSON method. +type EncodeOpt func(e *json.Encoder) + +// EncodeOptHTMLEscape sets the encoder to escape the JSON for html. +func EncodeOptHTMLEscape(doEscape bool) EncodeOpt { + return func(e *json.Encoder) { + e.SetEscapeHTML(doEscape) + } +} + +// EncodeOptIndent sets the encoder to indent the JSON output. +func EncodeOptIndent(prefix string, indent string) EncodeOpt { + return func(e *json.Encoder) { + e.SetIndent(prefix, indent) + } +} + +// EncodeJSON - Encodes the contained object back to a JSON formatted []byte +// using a variant list of modifier functions for the encoder being used. +// Functions for modifying the output are prefixed with EncodeOpt, e.g. +// EncodeOptHTMLEscape. +func (g *Container) EncodeJSON(encodeOpts ...EncodeOpt) []byte { + var b bytes.Buffer + encoder := json.NewEncoder(&b) + encoder.SetEscapeHTML(false) // Do not escape by default. + for _, opt := range encodeOpts { + opt(encoder) + } + if err := encoder.Encode(g.object); err != nil { + return []byte("{}") + } + result := b.Bytes() + if len(result) > 0 { + result = result[:len(result)-1] + } + return result +} + +// New - Create a new gabs JSON object. +func New() *Container { + return &Container{map[string]interface{}{}} +} + +// Consume - Gobble up an already converted JSON object, or a fresh map[string]interface{} object. +func Consume(root interface{}) (*Container, error) { + return &Container{root}, nil +} + +// ParseJSON - Convert a string into a representation of the parsed JSON. +func ParseJSON(sample []byte) (*Container, error) { + var gabs Container + + if err := json.Unmarshal(sample, &gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +// ParseJSONDecoder - Convert a json.Decoder into a representation of the parsed JSON. +func ParseJSONDecoder(decoder *json.Decoder) (*Container, error) { + var gabs Container + + if err := decoder.Decode(&gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +// ParseJSONFile - Read a file and convert into a representation of the parsed JSON. +func ParseJSONFile(path string) (*Container, error) { + if len(path) > 0 { + cBytes, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + container, err := ParseJSON(cBytes) + if err != nil { + return nil, err + } + + return container, nil + } + return nil, ErrInvalidPath +} + +// ParseJSONBuffer - Read the contents of a buffer into a representation of the parsed JSON. +func ParseJSONBuffer(buffer io.Reader) (*Container, error) { + var gabs Container + jsonDecoder := json.NewDecoder(buffer) + if err := jsonDecoder.Decode(&gabs.object); err != nil { + return nil, err + } + + return &gabs, nil +} + +//-------------------------------------------------------------------------------------------------- diff --git a/vendor/github.com/Jeffail/gabs/gabs_logo.png b/vendor/github.com/Jeffail/gabs/gabs_logo.png new file mode 100644 index 0000000..e8c2832 Binary files /dev/null and b/vendor/github.com/Jeffail/gabs/gabs_logo.png differ diff --git a/vendor/github.com/Jeffail/gabs/gabs_test.go b/vendor/github.com/Jeffail/gabs/gabs_test.go new file mode 100644 index 0000000..800699d --- /dev/null +++ b/vendor/github.com/Jeffail/gabs/gabs_test.go @@ -0,0 +1,1211 @@ +package gabs + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "testing" +) + +func TestBasic(t *testing.T) { + sample := []byte(`{"test":{"value":10},"test2":20}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if result, ok := val.Search([]string{"test", "value"}...).Data().(float64); ok { + if result != 10 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find test.value") + } + + if _, ok := val.Search("test2", "value").Data().(string); ok { + t.Errorf("Somehow found a field that shouldn't exist") + } + + if result, ok := val.Search("test2").Data().(float64); ok { + if result != 20 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find test2") + } + + if result := val.Bytes(); string(result) != string(sample) { + t.Errorf("Wrong []byte conversion: %s != %s", result, sample) + } +} + +func TestExists(t *testing.T) { + sample := []byte(`{"test":{"value":10,"nullvalue":null},"test2":20,"testnull":null}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + paths := []struct { + Path []string + Exists bool + }{ + {[]string{"one", "two", "three"}, false}, + {[]string{"test"}, true}, + {[]string{"test", "value"}, true}, + {[]string{"test", "nullvalue"}, true}, + {[]string{"test2"}, true}, + {[]string{"testnull"}, true}, + {[]string{"test2", "value"}, false}, + {[]string{"test", "value2"}, false}, + {[]string{"test", "VALUE"}, false}, + } + + for _, p := range paths { + if exp, actual := p.Exists, val.Exists(p.Path...); exp != actual { + t.Errorf("Wrong result from Exists: %v != %v, for path: %v", exp, actual, p.Path) + } + if exp, actual := p.Exists, val.ExistsP(strings.Join(p.Path, ".")); exp != actual { + t.Errorf("Wrong result from ExistsP: %v != %v, for path: %v", exp, actual, p.Path) + } + } +} + +func TestExistsWithArrays(t *testing.T) { + sample := []byte(`{"foo":{"bar":{"baz":[10, 2, 3]}}}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + + sample = []byte(`{"foo":{"bar":[{"baz":10},{"baz":2},{"baz":3}]}}`) + + if val, err = ParseJSON(sample); err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + if exp, actual := false, val.Exists("foo", "bar", "baz_NOPE"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + + sample = []byte(`{"foo":[{"bar":{"baz":10}},{"bar":{"baz":2}},{"bar":{"baz":3}}]}`) + + if val, err = ParseJSON(sample); err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + if exp, actual := false, val.Exists("foo", "bar", "baz_NOPE"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + + sample = + []byte(`[{"foo":{"bar":{"baz":10}}},{"foo":{"bar":{"baz":2}}},{"foo":{"bar":{"baz":3}}}]`) + + if val, err = ParseJSON(sample); err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if exp, actual := true, val.Exists("foo", "bar", "baz"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } + if exp, actual := false, val.Exists("foo", "bar", "baz_NOPE"); exp != actual { + t.Errorf("Wrong result from array based Exists: %v != %v", exp, actual) + } +} + +func TestBasicWithBuffer(t *testing.T) { + sample := bytes.NewReader([]byte(`{"test":{"value":10},"test2":20}`)) + + _, err := ParseJSONBuffer(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } +} + +func TestBasicWithDecoder(t *testing.T) { + sample := []byte(`{"test":{"int":10, "float":6.66}}`) + dec := json.NewDecoder(bytes.NewReader(sample)) + dec.UseNumber() + + val, err := ParseJSONDecoder(dec) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + checkNumber := func(path string, expectedVal json.Number) { + data := val.Path(path).Data() + asNumber, isNumber := data.(json.Number) + if !isNumber { + t.Error("Failed to parse using decoder UseNumber policy") + } + if expectedVal != asNumber { + t.Errorf("Expected[%s] but got [%s]", expectedVal, asNumber) + } + } + + checkNumber("test.int", "10") + checkNumber("test.float", "6.66") +} + +func TestFailureWithDecoder(t *testing.T) { + sample := []byte(`{"test":{" "invalidCrap":.66}}`) + dec := json.NewDecoder(bytes.NewReader(sample)) + + _, err := ParseJSONDecoder(dec) + if err == nil { + t.Fatal("Expected parsing error") + } +} + +func TestFindArray(t *testing.T) { + for i, this := range []struct { + input string + target string + expect string + }{ + { + `{"test":{"array":[{"value":1}, {"value":2}, {"value":3}]}}`, + "test.array.value", + "[1,2,3]", + }, + { + `{ + "test":{ + "array":[ + { + "values":[ + {"more":1}, + {"more":2}, + {"more":3} + ] + }, + { + "values":[ + {"more":4}, + {"more":5}, + {"more":6} + ] + }, + { + "values":[ + {"more":7}, + {"more":8}, + {"more":9} + ] + } + ] + } + }`, + "test.array.values.more", + "[[1,2,3],[4,5,6],[7,8,9]]", + }, + } { + val, err := ParseJSON([]byte(this.input)) + if err != nil { + t.Errorf("[%d] Failed to parse: %s", i, err) + return + } + + target := val.Path(this.target) + result := target.String() + + if this.expect != result { + t.Errorf("[%d] Expected %v, received %v", i, this.expect, result) + } + } +} + +func TestDeletes(t *testing.T) { + jsonParsed, _ := ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value1":10, + "value2":22, + "value3":32 + }, + "alsoInner":{ + "value1":20, + "value2":42, + "value3":92 + }, + "another":{ + "value1":null, + "value2":null, + "value3":null + } + } + }`)) + + if err := jsonParsed.Delete("outter", "inner", "value2"); err != nil { + t.Error(err) + } + if err := jsonParsed.Delete("outter", "inner", "value4"); err == nil { + t.Error(fmt.Errorf("value4 should not have been found in outter.inner")) + } + if err := jsonParsed.Delete("outter", "another", "value1"); err != nil { + t.Error(err) + } + if err := jsonParsed.Delete("outter", "another", "value4"); err == nil { + t.Error(fmt.Errorf("value4 should not have been found in outter.another")) + } + if err := jsonParsed.DeleteP("outter.alsoInner.value1"); err != nil { + t.Error(err) + } + if err := jsonParsed.DeleteP("outter.alsoInner.value4"); err == nil { + t.Error(fmt.Errorf("value4 should not have been found in outter.alsoInner")) + } + if err := jsonParsed.DeleteP("outter.another.value2"); err != nil { + t.Error(err) + } + if err := jsonParsed.Delete("outter.another.value4"); err == nil { + t.Error(fmt.Errorf("value4 should not have been found in outter.another")) + } + + expected := `{"outter":{"alsoInner":{"value2":42,"value3":92},"another":{"value3":null},"inner":{"value1":10,"value3":32}}}` + if actual := jsonParsed.String(); actual != expected { + t.Errorf("Unexpected result from deletes: %v != %v", actual, expected) + } +} + +func TestExamples(t *testing.T) { + jsonParsed, _ := ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value1":10, + "value2":22 + }, + "alsoInner":{ + "value1":20 + } + } + }`)) + + var value float64 + var ok bool + + value, ok = jsonParsed.Path("outter.inner.value1").Data().(float64) + if value != 10.0 || !ok { + t.Errorf("wrong value: %v, %v", value, ok) + } + + value, ok = jsonParsed.Search("outter", "inner", "value1").Data().(float64) + if value != 10.0 || !ok { + t.Errorf("wrong value: %v, %v", value, ok) + } + + value, ok = jsonParsed.Path("does.not.exist").Data().(float64) + if value != 0.0 || ok { + t.Errorf("wrong value: %v, %v", value, ok) + } + + jsonParsed, _ = ParseJSON([]byte(`{"array":[ "first", "second", "third" ]}`)) + + expected := []string{"first", "second", "third"} + + children, err := jsonParsed.S("array").Children() + if err != nil { + t.Errorf("Error: %v", err) + return + } + for i, child := range children { + if expected[i] != child.Data().(string) { + t.Errorf("Child unexpected: %v != %v", expected[i], child.Data().(string)) + } + } +} + +func TestExamples2(t *testing.T) { + var err error + + jsonObj := New() + + _, err = jsonObj.Set(10, "outter", "inner", "value") + if err != nil { + t.Errorf("Error: %v", err) + return + } + _, err = jsonObj.SetP(20, "outter.inner.value2") + if err != nil { + t.Errorf("Error: %v", err) + return + } + _, err = jsonObj.Set(30, "outter", "inner2", "value3") + if err != nil { + t.Errorf("Error: %v", err) + return + } + + expected := `{"outter":{"inner":{"value":10,"value2":20},"inner2":{"value3":30}}}` + if jsonObj.String() != expected { + t.Errorf("Non matched output: %v != %v", expected, jsonObj.String()) + } + + jsonObj, _ = Consume(map[string]interface{}{}) + + jsonObj.Array("array") + + jsonObj.ArrayAppend(10, "array") + jsonObj.ArrayAppend(20, "array") + jsonObj.ArrayAppend(30, "array") + + expected = `{ + "array": [ + 10, + 20, + 30 + ] + }` + result := jsonObj.StringIndent(" ", " ") + if result != expected { + t.Errorf("Non matched output: %v != %v", expected, result) + } +} + +func TestExamples3(t *testing.T) { + jsonObj := New() + + jsonObj.ArrayP("foo.array") + + jsonObj.ArrayAppend(10, "foo", "array") + jsonObj.ArrayAppend(20, "foo", "array") + jsonObj.ArrayAppend(30, "foo", "array") + + result := jsonObj.String() + expected := `{"foo":{"array":[10,20,30]}}` + + if result != expected { + t.Errorf("Non matched output: %v != %v", result, expected) + } +} + +func TestDotNotation(t *testing.T) { + sample := []byte(`{"test":{"inner":{"value":10}},"test2":20}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if result, _ := val.Path("test.inner.value").Data().(float64); result != 10 { + t.Errorf("Expected 10, received: %v", result) + } +} + +func TestModify(t *testing.T) { + sample := []byte(`{"test":{"value":10},"test2":20}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if _, err := val.S("test").Set(45.0, "value"); err != nil { + t.Errorf("Failed to set field") + } + + if result, ok := val.Search([]string{"test", "value"}...).Data().(float64); ok { + if result != 45 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find test.value") + } + + if out := val.String(); `{"test":{"value":45},"test2":20}` != out { + t.Errorf("Incorrectly serialized: %v", out) + } + + if out := val.Search("test").String(); `{"value":45}` != out { + t.Errorf("Incorrectly serialized: %v", out) + } +} + +func TestChildren(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "objectOne":{ + }, + "objectTwo":{ + }, + "objectThree":{ + } + }`)) + + objects, _ := json1.Children() + for _, object := range objects { + object.Set("hello world", "child") + } + + expected := `{"objectOne":{"child":"hello world"},"objectThree":{"child":"hello world"}` + + `,"objectTwo":{"child":"hello world"}}` + received := json1.String() + if expected != received { + t.Errorf("json1: expected %v, received %v", expected, received) + } + + json2, _ := ParseJSON([]byte(`{ + "values":[ + { + "objectOne":{ + } + }, + { + "objectTwo":{ + } + }, + { + "objectThree":{ + } + } + ] + }`)) + + json3, _ := ParseJSON([]byte(`{ + "values":[ + ] + }`)) + + numChildren1, _ := json2.ArrayCount("values") + numChildren2, _ := json3.ArrayCount("values") + if _, err := json3.ArrayCount("valuesNOTREAL"); err == nil { + t.Errorf("expected numChildren3 to fail") + } + + if numChildren1 != 3 || numChildren2 != 0 { + t.Errorf("CountElements, expected 3 and 0, received %v and %v", + numChildren1, numChildren2) + } + + objects, _ = json2.S("values").Children() + for _, object := range objects { + object.Set("hello world", "child") + json3.ArrayAppend(object.Data(), "values") + } + + expected = `{"values":[{"child":"hello world","objectOne":{}},{"child":"hello world",` + + `"objectTwo":{}},{"child":"hello world","objectThree":{}}]}` + received = json2.String() + if expected != received { + t.Errorf("json2: expected %v, received %v", expected, received) + } + + received = json3.String() + if expected != received { + t.Errorf("json3: expected %v, received %v", expected, received) + } +} + +func TestChildrenMap(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "objectOne":{"num":1}, + "objectTwo":{"num":2}, + "objectThree":{"num":3} + }`)) + + objectMap, err := json1.ChildrenMap() + if err != nil { + t.Error(err) + return + } + + if len(objectMap) != 3 { + t.Errorf("Wrong num of elements in objectMap: %v != %v", len(objectMap), 3) + return + } + + for key, val := range objectMap { + if "objectOne" == key { + if val := val.S("num").Data().(float64); val != 1 { + t.Errorf("%v != %v", val, 1) + } + } else if "objectTwo" == key { + if val := val.S("num").Data().(float64); val != 2 { + t.Errorf("%v != %v", val, 2) + } + } else if "objectThree" == key { + if val := val.S("num").Data().(float64); val != 3 { + t.Errorf("%v != %v", val, 3) + } + } else { + t.Errorf("Unexpected key: %v", key) + } + } + + objectMap["objectOne"].Set(500, "num") + if val := json1.Path("objectOne.num").Data().(int); val != 500 { + t.Errorf("set objectOne failed: %v != %v", val, 500) + } +} + +func TestNestedAnonymousArrays(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "array":[ + [ 1, 2, 3 ], + [ 4, 5, 6 ], + [ 7, 8, 9 ], + [{ "test" : 50 }] + ] + }`)) + + childTest, err := json1.S("array").Index(0).Children() + if err != nil { + t.Error(err) + return + } + + if val := childTest[0].Data().(float64); val != 1 { + t.Errorf("child test: %v != %v", val, 1) + } + if val := childTest[1].Data().(float64); val != 2 { + t.Errorf("child test: %v != %v", val, 2) + } + if val := childTest[2].Data().(float64); val != 3 { + t.Errorf("child test: %v != %v", val, 3) + } + + if val := json1.Path("array").Index(1).Index(1).Data().(float64); val != 5 { + t.Errorf("nested child test: %v != %v", val, 5) + } + + if val := json1.Path("array").Index(3).Index(0).S("test").Data().(float64); val != 50 { + t.Errorf("nested child object test: %v != %v", val, 50) + } + + json1.Path("array").Index(3).Index(0).Set(200, "test") + + if val := json1.Path("array").Index(3).Index(0).S("test").Data().(int); val != 200 { + t.Errorf("set nested child object: %v != %v", val, 200) + } +} + +func TestArrays(t *testing.T) { + json1, _ := ParseJSON([]byte(`{ + "languages":{ + "english":{ + "places":0 + }, + "french": { + "places": [ + "france", + "belgium" + ] + } + } + }`)) + + json2, _ := ParseJSON([]byte(`{ + "places":[ + "great_britain", + "united_states_of_america", + "the_world" + ] + }`)) + + if englishPlaces := json2.Search("places").Data(); englishPlaces != nil { + json1.Path("languages.english").Set(englishPlaces, "places") + } else { + t.Errorf("Didn't find places in json2") + } + + if englishPlaces := json1.Search("languages", "english", "places").Data(); englishPlaces != nil { + + englishArray, ok := englishPlaces.([]interface{}) + if !ok { + t.Errorf("places in json1 (%v) was not an array", englishPlaces) + } + + if len(englishArray) != 3 { + t.Errorf("wrong length of array: %v", len(englishArray)) + } + + } else { + t.Errorf("Didn't find places in json1") + } + + for i := 0; i < 3; i++ { + if err := json2.ArrayRemove(0, "places"); err != nil { + t.Errorf("Error removing element: %v", err) + } + } + + json2.ArrayAppend(map[string]interface{}{}, "places") + json2.ArrayAppend(map[string]interface{}{}, "places") + json2.ArrayAppend(map[string]interface{}{}, "places") + + // Using float64 for this test even though it's completely inappropriate because + // later on the API might do something clever with types, in which case all numbers + // will become float64. + for i := 0; i < 3; i++ { + obj, _ := json2.ArrayElement(i, "places") + obj2, _ := obj.Object(fmt.Sprintf("object%v", i)) + obj2.Set(float64(i), "index") + } + + children, _ := json2.S("places").Children() + for i, obj := range children { + if id, ok := obj.S(fmt.Sprintf("object%v", i)).S("index").Data().(float64); ok { + if id != float64(i) { + t.Errorf("Wrong index somehow, expected %v, received %v", i, id) + } + } else { + t.Errorf("Failed to find element %v from %v", i, obj) + } + } + + if err := json2.ArrayRemove(1, "places"); err != nil { + t.Errorf("Error removing element: %v", err) + } + + expected := `{"places":[{"object0":{"index":0}},{"object2":{"index":2}}]}` + received := json2.String() + + if expected != received { + t.Errorf("Wrong output, expected: %v, received: %v", expected, received) + } +} + +func TestArraysTwo(t *testing.T) { + json1 := New() + + test1, err := json1.ArrayOfSize(4, "test1") + if err != nil { + t.Error(err) + } + + if _, err = test1.ArrayOfSizeI(2, 0); err != nil { + t.Error(err) + } + if _, err = test1.ArrayOfSizeI(2, 1); err != nil { + t.Error(err) + } + if _, err = test1.ArrayOfSizeI(2, 2); err != nil { + t.Error(err) + } + if _, err = test1.ArrayOfSizeI(2, 3); err != nil { + t.Error(err) + } + + if _, err = test1.ArrayOfSizeI(2, 4); err != ErrOutOfBounds { + t.Errorf("Index should have been out of bounds") + } + + if _, err = json1.S("test1").Index(0).SetIndex(10, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(0).SetIndex(11, 1); err != nil { + t.Error(err) + } + + if _, err = json1.S("test1").Index(1).SetIndex(12, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(1).SetIndex(13, 1); err != nil { + t.Error(err) + } + + if _, err = json1.S("test1").Index(2).SetIndex(14, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(2).SetIndex(15, 1); err != nil { + t.Error(err) + } + + if _, err = json1.S("test1").Index(3).SetIndex(16, 0); err != nil { + t.Error(err) + } + if _, err = json1.S("test1").Index(3).SetIndex(17, 1); err != nil { + t.Error(err) + } + + if val := json1.S("test1").Index(0).Index(0).Data().(int); val != 10 { + t.Errorf("create array: %v != %v", val, 10) + } + if val := json1.S("test1").Index(0).Index(1).Data().(int); val != 11 { + t.Errorf("create array: %v != %v", val, 11) + } + + if val := json1.S("test1").Index(1).Index(0).Data().(int); val != 12 { + t.Errorf("create array: %v != %v", val, 12) + } + if val := json1.S("test1").Index(1).Index(1).Data().(int); val != 13 { + t.Errorf("create array: %v != %v", val, 13) + } + + if val := json1.S("test1").Index(2).Index(0).Data().(int); val != 14 { + t.Errorf("create array: %v != %v", val, 14) + } + if val := json1.S("test1").Index(2).Index(1).Data().(int); val != 15 { + t.Errorf("create array: %v != %v", val, 15) + } + + if val := json1.S("test1").Index(3).Index(0).Data().(int); val != 16 { + t.Errorf("create array: %v != %v", val, 16) + } + if val := json1.S("test1").Index(3).Index(1).Data().(int); val != 17 { + t.Errorf("create array: %v != %v", val, 17) + } +} + +func TestArraysThree(t *testing.T) { + json1 := New() + + test, err := json1.ArrayOfSizeP(1, "test1.test2") + if err != nil { + t.Error(err) + } + + test.SetIndex(10, 0) + if val := json1.S("test1", "test2").Index(0).Data().(int); val != 10 { + t.Error(err) + } +} + +func TestArraysRoot(t *testing.T) { + sample := []byte(`["test1"]`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + val.ArrayAppend("test2") + val.ArrayAppend("test3") + if obj, err := val.ObjectI(2); err != nil { + t.Error(err) + } else { + obj.Set("bar", "foo") + } + + if expected, actual := `["test1","test2",{"foo":"bar"}]`, val.String(); expected != actual { + t.Errorf("expected %v, received: %v", expected, actual) + } +} + +func TestLargeSample(t *testing.T) { + sample := []byte(`{ + "test":{ + "innerTest":{ + "value":10, + "value2":22, + "value3":{ + "moreValue":45 + } + } + }, + "test2":20 + }`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + if result, ok := val.Search("test", "innerTest", "value3", "moreValue").Data().(float64); ok { + if result != 45 { + t.Errorf("Wrong value of result: %v", result) + } + } else { + t.Errorf("Didn't find value") + } +} + +func TestShorthand(t *testing.T) { + json, _ := ParseJSON([]byte(`{ + "outter":{ + "inner":{ + "value":5, + "value2":10, + "value3":11 + }, + "inner2":{ + } + }, + "outter2":{ + "inner":0 + } + }`)) + + missingValue := json.S("outter").S("doesntexist").S("alsodoesntexist").S("inner").S("value").Data() + if missingValue != nil { + t.Errorf("missing value was actually found: %v\n", missingValue) + } + + realValue := json.S("outter").S("inner").S("value2").Data().(float64) + if realValue != 10 { + t.Errorf("real value was incorrect: %v\n", realValue) + } + + _, err := json.S("outter2").Set(json.S("outter").S("inner").Data(), "inner") + if err != nil { + t.Errorf("error setting outter2: %v\n", err) + } + + compare := `{"outter":{"inner":{"value":5,"value2":10,"value3":11},"inner2":{}}` + + `,"outter2":{"inner":{"value":5,"value2":10,"value3":11}}}` + out := json.String() + if out != compare { + t.Errorf("wrong serialized structure: %v\n", out) + } + + compare2 := `{"outter":{"inner":{"value":6,"value2":10,"value3":11},"inner2":{}}` + + `,"outter2":{"inner":{"value":6,"value2":10,"value3":11}}}` + + json.S("outter").S("inner").Set(6, "value") + out = json.String() + if out != compare2 { + t.Errorf("wrong serialized structure: %v\n", out) + } +} + +func TestInvalid(t *testing.T) { + invalidJSONSamples := []string{ + `{dfads"`, + ``, + // `""`, + // `"hello"`, + "{}\n{}", + } + + for _, sample := range invalidJSONSamples { + if _, err := ParseJSON([]byte(sample)); err == nil { + t.Errorf("parsing invalid JSON '%v' did not return error", sample) + } + } + + if _, err := ParseJSON(nil); err == nil { + t.Errorf("parsing nil did not return error") + } + + validObj, err := ParseJSON([]byte(`{}`)) + if err != nil { + t.Errorf("failed to parse '{}'") + } + + invalidStr := validObj.S("Doesn't exist").String() + if "{}" != invalidStr { + t.Errorf("expected '{}', received: %v", invalidStr) + } +} + +func TestCreation(t *testing.T) { + json, _ := ParseJSON([]byte(`{}`)) + inner, err := json.ObjectP("test.inner") + if err != nil { + t.Errorf("Error: %v", err) + return + } + + inner.Set(10, "first") + inner.Set(20, "second") + + inner.Array("array") + inner.ArrayAppend("first element of the array", "array") + inner.ArrayAppend(2, "array") + inner.ArrayAppend("three", "array") + + expected := `{"test":{"inner":{"array":["first element of the array",2,"three"],` + + `"first":10,"second":20}}}` + actual := json.String() + if actual != expected { + t.Errorf("received incorrect output from json object: %v\n", actual) + } +} + +type outterJSON struct { + FirstInner innerJSON + SecondInner innerJSON + ThirdInner innerJSON +} + +type innerJSON struct { + NumberType float64 + StringType string +} + +type jsonStructure struct { + FirstOutter outterJSON + SecondOutter outterJSON +} + +var jsonContent = []byte(`{ + "firstOutter":{ + "firstInner":{ + "numberType":11, + "stringType":"hello world, first first" + }, + "secondInner":{ + "numberType":12, + "stringType":"hello world, first second" + }, + "thirdInner":{ + "numberType":13, + "stringType":"hello world, first third" + } + }, + "secondOutter":{ + "firstInner":{ + "numberType":21, + "stringType":"hello world, second first" + }, + "secondInner":{ + "numberType":22, + "stringType":"hello world, second second" + }, + "thirdInner":{ + "numberType":23, + "stringType":"hello world, second third" + } + } +}`) + +/* +Simple use case, compares unmarshalling declared structs vs dynamically searching for +the equivalent hierarchy. Hopefully we won't see too great a performance drop from the +dynamic approach. +*/ + +func BenchmarkStatic(b *testing.B) { + for i := 0; i < b.N; i++ { + var jsonObj jsonStructure + json.Unmarshal(jsonContent, &jsonObj) + + if val := jsonObj.FirstOutter.SecondInner.NumberType; val != 12 { + b.Errorf("Wrong value of FirstOutter.SecondInner.NumberType: %v\n", val) + } + expected := "hello world, first second" + if val := jsonObj.FirstOutter.SecondInner.StringType; val != expected { + b.Errorf("Wrong value of FirstOutter.SecondInner.StringType: %v\n", val) + } + if val := jsonObj.SecondOutter.ThirdInner.NumberType; val != 23 { + b.Errorf("Wrong value of SecondOutter.ThirdInner.NumberType: %v\n", val) + } + expected = "hello world, second second" + if val := jsonObj.SecondOutter.SecondInner.StringType; val != expected { + b.Errorf("Wrong value of SecondOutter.SecondInner.StringType: %v\n", val) + } + } +} + +func BenchmarkDynamic(b *testing.B) { + for i := 0; i < b.N; i++ { + jsonObj, err := ParseJSON(jsonContent) + if err != nil { + b.Errorf("Error parsing json: %v\n", err) + } + + FOSI := jsonObj.S("firstOutter", "secondInner") + SOSI := jsonObj.S("secondOutter", "secondInner") + SOTI := jsonObj.S("secondOutter", "thirdInner") + + if val := FOSI.S("numberType").Data().(float64); val != 12 { + b.Errorf("Wrong value of FirstOutter.SecondInner.NumberType: %v\n", val) + } + expected := "hello world, first second" + if val := FOSI.S("stringType").Data().(string); val != expected { + b.Errorf("Wrong value of FirstOutter.SecondInner.StringType: %v\n", val) + } + if val := SOTI.S("numberType").Data().(float64); val != 23 { + b.Errorf("Wrong value of SecondOutter.ThirdInner.NumberType: %v\n", val) + } + expected = "hello world, second second" + if val := SOSI.S("stringType").Data().(string); val != expected { + b.Errorf("Wrong value of SecondOutter.SecondInner.StringType: %v\n", val) + } + } +} + +func TestNoTypeChildren(t *testing.T) { + jsonObj, err := ParseJSON([]byte(`{"not_obj_or_array":1}`)) + if err != nil { + t.Error(err) + } + exp := ErrNotObjOrArray + if _, act := jsonObj.S("not_obj_or_array").Children(); act != exp { + t.Errorf("Unexpected value returned: %v != %v", exp, act) + } + exp = ErrNotObj + if _, act := jsonObj.S("not_obj_or_array").ChildrenMap(); act != exp { + t.Errorf("Unexpected value returned: %v != %v", exp, act) + } +} + +func TestBadIndexes(t *testing.T) { + jsonObj, err := ParseJSON([]byte(`{"array":[1,2,3]}`)) + if err != nil { + t.Error(err) + } + if act := jsonObj.Index(0).Data(); nil != act { + t.Errorf("Unexpected value returned: %v != %v", nil, act) + } + if act := jsonObj.S("array").Index(4).Data(); nil != act { + t.Errorf("Unexpected value returned: %v != %v", nil, act) + } +} + +func TestNilSet(t *testing.T) { + obj := Container{nil} + if _, err := obj.Set("bar", "foo"); err != nil { + t.Error(err) + } + if _, err := obj.Set("new", "foo", "bar"); err != ErrPathCollision { + t.Errorf("Expected ErrPathCollision: %v, %s", err, obj.Data()) + } + if _, err := obj.SetIndex("new", 0); err != ErrNotArray { + t.Errorf("Expected ErrNotArray: %v, %s", err, obj.Data()) + } +} + +func TestLargeSampleWithHtmlEscape(t *testing.T) { + sample := []byte(`{ + "test": { + "innerTest": { + "value": 10, + "value2": "Title", + "value3": { + "moreValue": 45 + } + } + }, + "test2": 20 +}`) + + sampleWithHTMLEscape := []byte(`{ + "test": { + "innerTest": { + "value": 10, + "value2": "\u003ctitle\u003eTitle\u003c/title\u003e", + "value3": { + "moreValue": 45 + } + } + }, + "test2": 20 +}`) + + val, err := ParseJSON(sample) + if err != nil { + t.Errorf("Failed to parse: %v", err) + return + } + + exp := string(sample) + res := string(val.EncodeJSON(EncodeOptIndent("", "\t"))) + if exp != res { + t.Errorf("Wrong conversion without html escaping: %s != %s", res, exp) + } + + exp = string(sampleWithHTMLEscape) + res = string(val.EncodeJSON(EncodeOptHTMLEscape(true), EncodeOptIndent("", "\t"))) + if exp != res { + t.Errorf("Wrong conversion with html escaping: %s != %s", exp, res) + } +} + +func TestMergeCases(t *testing.T) { + type testCase struct { + first string + second string + expected string + } + + testCases := []testCase{ + { + first: `{"outter":{"value1":"one"}}`, + second: `{"outter":{"inner":{"value3": "threre"}},"outter2":{"value2": "two"}}`, + expected: `{"outter":{"inner":{"value3":"threre"},"value1":"one"},"outter2":{"value2":"two"}}`, + }, + { + first: `{"outter":["first"]}`, + second: `{"outter":["second"]}`, + expected: `{"outter":["first","second"]}`, + }, + { + first: `{"outter":["first",{"inner":"second"}]}`, + second: `{"outter":["third"]}`, + expected: `{"outter":["first",{"inner":"second"},"third"]}`, + }, + { + first: `{"outter":["first",{"inner":"second"}]}`, + second: `{"outter":"third"}`, + expected: `{"outter":["first",{"inner":"second"},"third"]}`, + }, + { + first: `{"outter":"first"}`, + second: `{"outter":"second"}`, + expected: `{"outter":["first","second"]}`, + }, + { + first: `{"outter":{"inner":"first"}}`, + second: `{"outter":{"inner":"second"}}`, + expected: `{"outter":{"inner":["first","second"]}}`, + }, + { + first: `{"outter":{"inner":"first"}}`, + second: `{"outter":"second"}`, + expected: `{"outter":[{"inner":"first"},"second"]}`, + }, + { + first: `{"outter":{"inner":"second"}}`, + second: `{"outter":{"inner":{"inner2":"first"}}}`, + expected: `{"outter":{"inner":["second",{"inner2":"first"}]}}`, + }, + { + first: `{"outter":{"inner":["second"]}}`, + second: `{"outter":{"inner":{"inner2":"first"}}}`, + expected: `{"outter":{"inner":["second",{"inner2":"first"}]}}`, + }, + { + first: `{"outter":"second"}`, + second: `{"outter":{"inner":"first"}}`, + expected: `{"outter":["second",{"inner":"first"}]}`, + }, + } + + for i, test := range testCases { + var firstContainer, secondContainer *Container + var err error + + firstContainer, err = ParseJSON([]byte(test.first)) + if err != nil { + t.Errorf("[%d] Failed to parse '%v': %v", i, test.first, err) + } + + secondContainer, err = ParseJSON([]byte(test.second)) + if err != nil { + t.Errorf("[%d] Failed to parse '%v': %v", i, test.second, err) + } + + if err = firstContainer.Merge(secondContainer); err != nil { + t.Errorf("[%d] Failed to merge: '%v': %v", i, test.first, err) + } + + if exp, act := test.expected, firstContainer.String(); exp != act { + t.Errorf("[%d] Wrong result: %v != %v", i, act, exp) + } + } +} diff --git a/vendor/github.com/konsorten/go-gravitee/.gitignore b/vendor/github.com/konsorten/go-gravitee/.gitignore new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/.gitignore @@ -0,0 +1 @@ + diff --git a/vendor/github.com/konsorten/go-gravitee/.vscode/extensions.json b/vendor/github.com/konsorten/go-gravitee/.vscode/extensions.json new file mode 100644 index 0000000..a99afa2 --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + "heaths.vscode-guid", // Insert GUID + "streetsidesoftware.code-spell-checker", // Spelling and Grammar Checker + "lukehoban.go" // Go language + ] +} \ No newline at end of file diff --git a/vendor/github.com/konsorten/go-gravitee/.vscode/settings.json b/vendor/github.com/konsorten/go-gravitee/.vscode/settings.json new file mode 100644 index 0000000..f037ebe --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "Zapi", + "Zevenet", + "gravitee" + ] +} \ No newline at end of file diff --git a/vendor/github.com/konsorten/go-gravitee/LICENSE b/vendor/github.com/konsorten/go-gravitee/LICENSE new file mode 100644 index 0000000..a67d1a8 --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/LICENSE @@ -0,0 +1,9 @@ +(The MIT License) + +Copyright (c) 2018 marvin + konsorten GmbH (info@konsorten.de) + +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. diff --git a/vendor/github.com/konsorten/go-gravitee/README.md b/vendor/github.com/konsorten/go-gravitee/README.md new file mode 100644 index 0000000..e0997b4 --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/README.md @@ -0,0 +1,41 @@ +# Gravitee Management API for Go + +This library provides access to the Gravitee Management API (http://gravitee.io/). + +**[Click here to open the GoDoc documentation.](https://godoc.org/github.com/konsorten/go-gravitee)** + +## Usage + +To use the API, simply create a new session: + +```go +import "github.com/konsorten/go-gravitee" + +func main() { + session, _ := zevenet.Connect("api.mygravitee.com", "admin", "admin", nil) + + apis, _ := session.GetAllAPIs() +} +``` + +## Authors + +The library is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de). + +It is loosely based on the [go-bigip package](https://github.com/scottdware/go-bigip) by Scott Ware (and others). + +We thank all the authors who provided code to this library: + +* Felix Kollmann + +## License + +(The MIT License) + +Copyright (c) 2018 marvin + konsorten GmbH (open-source@konsorten.de) + +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. diff --git a/vendor/github.com/konsorten/go-gravitee/godoc.go b/vendor/github.com/konsorten/go-gravitee/godoc.go new file mode 100644 index 0000000..0268cab --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/godoc.go @@ -0,0 +1,4 @@ +// Package gravitee provides access to the Gravitee Management API (http://gravitee.io/). +// +// More information is available on the project website: https://github.com/konsorten/go-gravitee/ +package gravitee diff --git a/vendor/github.com/konsorten/go-gravitee/gravitee.go b/vendor/github.com/konsorten/go-gravitee/gravitee.go new file mode 100644 index 0000000..2277c82 --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/gravitee.go @@ -0,0 +1,301 @@ +package gravitee + +import ( + "bytes" + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "strings" + "time" +) + +var defaultConfigOptions = &ConfigOptions{ + APICallTimeout: 60 * time.Second, +} + +// ConfigOptions contains some advanced settings on server communication. +type ConfigOptions struct { + APICallTimeout time.Duration +} + +// GraviteeSession is a container for our session state. +type GraviteeSession struct { + Host string + Authorization string + Transport *http.Transport + ConfigOptions *ConfigOptions +} + +// String returns the session's hostname. +func (s *GraviteeSession) String() string { + return s.Host +} + +// APIRequest builds our request before sending it to the server. +type APIRequest struct { + Method string + URL string + Body string + ContentType string +} + +// RequestError contains information about any error we get from a request. +type RequestError struct { + Message string `json:"message,omitempty"` + HttpStatus int `json:"http_status,omitempty"` +} + +// Error returns the error message. +func (r RequestError) Error() string { + return fmt.Sprintf("%v (HTTP: %v)", r.Message, r.HttpStatus) +} + +// Connect sets up our connection to the Zevenet system. +func Connect(host, username, password string, configOptions *ConfigOptions) (*GraviteeSession, error) { + var url string + if !strings.HasPrefix(host, "http") { + url = fmt.Sprintf("https://%s", host) + } else { + url = host + } + if configOptions == nil { + configOptions = defaultConfigOptions + } + + // create the session + session := &GraviteeSession{ + Host: url, + Authorization: fmt.Sprintf("Basic %v", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%v:%v", username, password)))), + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + ConfigOptions: configOptions, + } + + // initialize the session + err := session.initialize() + + if err != nil { + return nil, err + } + + // done + return session, nil +} + +func (s *GraviteeSession) initialize() (err error) { + // test connection + _, err = s.Ping() + return +} + +// apiCall is used to query the ZAPI. +func (s *GraviteeSession) apiCall(options *APIRequest) ([]byte, error) { + var req *http.Request + client := &http.Client{ + Transport: s.Transport, + Timeout: s.ConfigOptions.APICallTimeout, + } + url := fmt.Sprintf("%v/management/%v", s.Host, options.URL) + body := bytes.NewReader([]byte(options.Body)) + req, _ = http.NewRequest(strings.ToUpper(options.Method), url, body) + + req.Header.Set("Authorization", s.Authorization) + + // fmt.Println("REQ -- ", options.Method, " ", url, " -- ", options.Body) + + if len(options.ContentType) > 0 { + req.Header.Set("Content-Type", options.ContentType) + } + + res, err := client.Do(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + data, _ := ioutil.ReadAll(res.Body) + + if res.StatusCode >= 400 { + if res.Header["Content-Type"][0] == "application/json" { + return data, s.checkError(data) + } + + return data, fmt.Errorf("HTTP %d :: %s", res.StatusCode, string(data[:])) + } + + // fmt.Println("Resp --", res.StatusCode, " -- ", string(data)) + return data, nil +} + +func (s *GraviteeSession) iControlPath(parts []string) string { + var buffer bytes.Buffer + for i, p := range parts { + buffer.WriteString(strings.Replace(p, "/", "~", -1)) + if i < len(parts)-1 { + buffer.WriteString("/") + } + } + return buffer.String() +} + +//Generic delete +func (s *GraviteeSession) delete(path ...string) error { + req := &APIRequest{ + Method: "delete", + URL: s.iControlPath(path), + } + + _, callErr := s.apiCall(req) + return callErr +} + +func (s *GraviteeSession) post(body interface{}, path ...string) error { + marshalJSON, err := jsonMarshal(body) + if err != nil { + return err + } + + req := &APIRequest{ + Method: "post", + URL: s.iControlPath(path), + Body: strings.TrimRight(string(marshalJSON), "\n"), + ContentType: "application/json", + } + + _, callErr := s.apiCall(req) + return callErr +} + +func (s *GraviteeSession) put(body interface{}, path ...string) error { + marshalJSON, err := jsonMarshal(body) + if err != nil { + return err + } + + return s.putRaw(marshalJSON, path...) +} + +func (s *GraviteeSession) putRaw(body []byte, path ...string) error { + req := &APIRequest{ + Method: "put", + URL: s.iControlPath(path), + Body: strings.TrimRight(string(body), "\n"), + ContentType: "application/json", + } + + _, callErr := s.apiCall(req) + return callErr +} + +//Get a url and populate an entity. If the entity does not exist (404) then the +//passed entity will be untouched and false will be returned as the second parameter. +//You can use this to distinguish between a missing entity or an actual error. +func (s *GraviteeSession) getForEntity(e interface{}, path ...string) error { + resp, err := s.getRaw(path...) + if err != nil { + return err + } + + err = json.Unmarshal(resp, e) + if err != nil { + return err + } + + return nil +} + +func (s *GraviteeSession) getRaw(path ...string) ([]byte, error) { + req := &APIRequest{ + Method: "get", + URL: s.iControlPath(path), + ContentType: "application/json", + } + + return s.apiCall(req) +} + +// checkError handles any errors we get from our API requests. It returns either the +// message of the error, if any, or nil. +func (s *GraviteeSession) checkError(resp []byte) error { + if len(resp) == 0 { + return nil + } + + var reqError RequestError + + err := json.Unmarshal(resp, &reqError) + if err != nil { + return fmt.Errorf("%s\n%s", err.Error(), string(resp[:])) + } + + return reqError +} + +// jsonMarshal specifies an encoder with 'SetEscapeHTML' set to 'false' so that <, >, and & are not escaped. https://golang.org/pkg/encoding/json/#Marshal +// https://stackoverflow.com/questions/28595664/how-to-stop-json-marshal-from-escaping-and +func jsonMarshal(t interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + err := encoder.Encode(t) + return buffer.Bytes(), err +} + +// Helper to copy between transfer objects and model objects to hide the myriad of boolean representations +// in the iControlREST api. DTO fields can be tagged with bool:"yes|enabled|true" to set what true and false +// marshal to. +func marshal(to, from interface{}) error { + toVal := reflect.ValueOf(to).Elem() + fromVal := reflect.ValueOf(from).Elem() + toType := toVal.Type() + for i := 0; i < toVal.NumField(); i++ { + toField := toVal.Field(i) + toFieldType := toType.Field(i) + fromField := fromVal.FieldByName(toFieldType.Name) + if fromField.Interface() != nil && fromField.Kind() == toField.Kind() { + toField.Set(fromField) + } else if toField.Kind() == reflect.Bool && fromField.Kind() == reflect.String { + switch fromField.Interface() { + case "yes", "enabled", "true": + toField.SetBool(true) + break + case "no", "disabled", "false", "": + toField.SetBool(false) + break + default: + return fmt.Errorf("Unknown boolean conversion for %s: %s", toFieldType.Name, fromField.Interface()) + } + } else if fromField.Kind() == reflect.Bool && toField.Kind() == reflect.String { + tag := toFieldType.Tag.Get("bool") + switch tag { + case "yes": + toField.SetString(toBoolString(fromField.Interface().(bool), "yes", "no")) + break + case "enabled": + toField.SetString(toBoolString(fromField.Interface().(bool), "enabled", "disabled")) + break + case "true": + toField.SetString(toBoolString(fromField.Interface().(bool), "true", "false")) + break + } + } else { + return fmt.Errorf("Unknown type conversion %s -> %s", fromField.Kind(), toField.Kind()) + } + } + return nil +} + +func toBoolString(b bool, trueStr, falseStr string) string { + if b { + return trueStr + } + return falseStr +} diff --git a/vendor/github.com/konsorten/go-gravitee/gravitee_apis.go b/vendor/github.com/konsorten/go-gravitee/gravitee_apis.go new file mode 100644 index 0000000..5b74987 --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/gravitee_apis.go @@ -0,0 +1,387 @@ +package gravitee + +import ( + "fmt" + "strings" + + "github.com/Jeffail/gabs" +) + +// ApiState is an enumeration of possible *State* to be used for an API. +type ApiState string + +const ( + ApiState_Started ApiState = "started" + ApiState_Stopped ApiState = "stopped" +) + +// ApiVisibility is an enumeration of possible *Visibility* to be used for an API. +type ApiVisibility string + +const ( + ApiVisibility_Private ApiVisibility = "private" + ApiVisibility_Public ApiVisibility = "public" +) + +type ApiInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Visibility ApiVisibility `json:"visibility"` + State ApiState `json:"state"` + Views []string `json:"views"` + Labels []string `json:"labels"` + Manageable bool `json:"manageable"` + NumberOfRatings int `json:"numberOfRatings"` + CreatedAt int `json:"created_at"` + UpdatedAdd int `json:"updated_at"` + Owner UserReference `json:"owner"` + PictureURL string `json:"picture_url"` + ContextPath string `json:"context_path"` +} + +func (ai ApiInfo) String() string { + return fmt.Sprintf("%v (%v, %v)", ai.Name, ai.ID, ai.State) +} + +// ApiMetadataFormat is an enumeration of possible *Format* to be used for an API metdata entry. +type ApiMetadataFormat string + +const ( + ApiMetadataFormat_String ApiMetadataFormat = "string" + ApiMetadataFormat_Numeric ApiMetadataFormat = "numeric" + ApiMetadataFormat_Boolean ApiMetadataFormat = "boolean" + ApiMetadataFormat_Date ApiMetadataFormat = "date" + ApiMetadataFormat_Mail ApiMetadataFormat = "mail" + ApiMetadataFormat_URL ApiMetadataFormat = "url" +) + +type ApiMetadata struct { + Key string `json:"key"` + Name string `json:"name"` + Format ApiMetadataFormat `json:"format"` + LocalValue string `json:"value"` + DefaultValue string `json:"defaultValue,omitempty"` + ApiID string `json:"apiId,omitempty"` +} + +func (ai ApiMetadata) Value() string { + if ai.LocalValue != "" { + return ai.LocalValue + } + + return ai.DefaultValue +} + +func (ai ApiMetadata) IsLocal() bool { + return ai.ApiID != "" +} + +func (ai ApiMetadata) String() string { + return fmt.Sprintf("%v = %v [%v]", ai.Name, ai.Value(), ai.Format) +} + +type ApiDetailsEndpointHttp struct { + ConnectTimeoutMS int `json:"connectTimeout"` + IdleTimeoutMS int `json:"idleTimeout"` + ReadTimeoutMS int `json:"readTimeout"` + KeepAlive bool `json:"keepAlive"` + Pipelining bool `json:"pipelining"` + MaxConcurrentConnections int `json:"maxConcurrentConnections"` + UseCompression bool `json:"useCompression"` + FollowRedirects bool `json:"followRedirects"` +} + +type ApiDetailsEndpointSSL struct { + IsEnabled bool `json:"enabled"` + TrustAllCertificates bool `json:"trustAll"` + VerifyHostnameInPublicCert bool `json:"hostnameVerifier"` + PublicCertPEM string `json:"pem"` +} + +type ApiDetailsEndpoint struct { + Name string `json:"name"` + Target string `json:"target"` + Weight int `json:"weight"` + IsBackup bool `json:"backup"` + Type string `json:"type"` + Http ApiDetailsEndpointHttp `json:"http"` + SSL ApiDetailsEndpointSSL `json:"ssl"` +} + +func (ai ApiDetailsEndpoint) String() string { + return fmt.Sprintf("%v (%v, %v)", ai.Name, ai.Target, ai.Type) +} + +func MakeApiDetailsEndpoint(name, target string) ApiDetailsEndpoint { + return ApiDetailsEndpoint{ + Name: name, + Target: target, + Weight: 1, + Type: "HTTP", + + Http: ApiDetailsEndpointHttp{ + ConnectTimeoutMS: 5000, + IdleTimeoutMS: 60000, + ReadTimeoutMS: 10000, + KeepAlive: true, + Pipelining: false, + MaxConcurrentConnections: 100, + UseCompression: true, + FollowRedirects: false, + }, + + SSL: ApiDetailsEndpointSSL{ + IsEnabled: false, + TrustAllCertificates: true, + VerifyHostnameInPublicCert: false, + PublicCertPEM: "", + }, + } +} + +type ApiDetailsPath struct { +} + +type ApiDetails struct { + ID string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Visibility ApiVisibility `json:"visibility"` + State ApiState `json:"state"` + Tags []string `json:"tags"` + Labels []string `json:"labels"` + //Paths map[string]ApiDetailsPath `json:"paths"` + CreatedAt int `json:"created_at"` + UpdatedAdd int `json:"updated_at"` + DeployedAt int `json:"deployed_at"` + Owner UserReference `json:"owner"` + PictureURL string `json:"picture_url"` + ContextPath string `json:"context_path"` + Proxy struct { + ContextPath string `json:"context_path"` + StripContextPath bool `json:"strip_context_path"` + LoggingMode string `json:"loggingMode"` + Endpoints []ApiDetailsEndpoint `json:"endpoints"` + + LoadBalancing struct { + Type string `json:"type"` + } `json:"load_balancing"` + + CORS struct { + IsEnabled bool `json:"enabled"` + AllowCredentials bool `json:"allowCredentials"` + AllowHeaders []string `json:"allowHeaders"` + AllowMethods []string `json:"allowMethods"` + AllowOrigin []string `json:"allowOrigin"` + ExposeHeaders []string `json:"exposeHeaders"` + MaxAgeSeconds int `json:"maxAge"` + } `json:"cors"` + } `json:"proxy"` +} + +func (ai ApiDetails) String() string { + return fmt.Sprintf("%v (%v, %v)", ai.Name, ai.ID, ai.State) +} + +// GetAllAPIs retrieves a list of all APIs registered in Gravitee. +func (s *GraviteeSession) GetAllAPIs() ([]ApiInfo, error) { + var result *[]ApiInfo + + err := s.getForEntity(&result, "apis") + if err != nil { + return nil, err + } + + return *result, nil +} + +// GetAPIsByLabel retrieves a list of all APIs registered in Gravitee. +func (s *GraviteeSession) GetAPIsByLabel(label string) ([]ApiInfo, error) { + result, err := s.GetAllAPIs() + if err != nil { + return nil, err + } + + filtered := make([]ApiInfo, 0) + + for _, ai := range result { + for _, lbl := range ai.Labels { + if strings.EqualFold(lbl, label) { + filtered = append(filtered, ai) + break + } + } + } + + return filtered, nil +} + +// GetAPI retrieves details on an API registered in Gravitee. +func (s *GraviteeSession) GetAPI(id string) (*ApiDetails, error) { + var result *ApiDetails + + err := s.getForEntity(&result, "apis", id) + if err != nil { + return nil, err + } + + return result, nil +} + +// AddOrUpdateEndpoints adds or updates an endpoint to an API registered in Gravitee. +func (s *GraviteeSession) AddOrUpdateEndpoints(id string, endpoints []ApiDetailsEndpoint, replaceAll bool) error { + res, err := s.getRaw("apis", id) + if err != nil { + return err + } + + json, err := gabs.ParseJSON(res) + if err != nil { + return err + } + + if replaceAll { + json.Array("proxy", "endpoints") + } + + for _, ep := range endpoints { + err := s.addOrUpdateEndpointSingle(json, ep) + if err != nil { + return err + } + } + + // clean-up (PUT request will fail if the following properties are set) + json.Delete("context_path") + json.Delete("created_at") + json.Delete("deployed_at") + json.Delete("id") + json.Delete("owner") + json.Delete("picture_url") + json.Delete("state") + json.Delete("updated_at") + + return s.putRaw(json.Bytes(), "apis", id) +} + +// AddOrUpdateEndpoint adds or updates an endpoint to an API registered in Gravitee. +func (s *GraviteeSession) addOrUpdateEndpointSingle(json *gabs.Container, endpoint ApiDetailsEndpoint) error { + endpointsArray := json.Search("proxy", "endpoints") + + // update existing endpoint + epcount, err := endpointsArray.ArrayCount() + if err != nil { + return err + } + + updated := false + + for i := 0; i < epcount; i++ { + ep, err := endpointsArray.ArrayElement(i) + if err != nil { + return err + } + + if endpoint.Name == ep.Search("name").Data() { + _, err = endpointsArray.SetIndex(endpoint, i) + + updated = true + break + } + } + + // add new endpoint + if !updated { + err = json.ArrayAppend(endpoint, "proxy", "endpoints") // does not work on endpointsArray + if err != nil { + return err + } + } + + return nil +} + +// GetAPIMetadata retrieves the metadata on an API registered in Gravitee. +func (s *GraviteeSession) GetAPIMetadata(id string) ([]ApiMetadata, error) { + var result *[]ApiMetadata + + err := s.getForEntity(&result, "apis", id, "metadata") + if err != nil { + return nil, err + } + + return *result, nil +} + +// GetLocalAPIMetadata retrieves a local metadata entry on an API registered in Gravitee. +func (s *GraviteeSession) GetLocalAPIMetadata(id string, metadataKey string) (*ApiMetadata, error) { + var result *ApiMetadata + + err := s.getForEntity(&result, "apis", id, "metadata", metadataKey) + if err != nil { + switch v := err.(type) { + case RequestError: + if v.HttpStatus == 404 { + return nil, nil + } + } + + return nil, err + } + + return result, nil +} + +// UnsetLocalAPIMetadata removes a local metadata entry on an API registered in Gravitee. +func (s *GraviteeSession) UnsetLocalAPIMetadata(id string, metadataKey string) error { + err := s.delete("apis", id, "metadata", metadataKey) + if err != nil { + switch v := err.(type) { + case RequestError: + if v.HttpStatus == 404 { + return nil + } + } + + return err + } + + return nil +} + +// SetLocalAPIMetadata updates or creates a local metadata entry on an API registered in Gravitee. +func (s *GraviteeSession) SetLocalAPIMetadata(id string, metadataKey string, value string, format ApiMetadataFormat) error { + req := ApiMetadata{ + Key: metadataKey, + Name: metadataKey, + LocalValue: value, + Format: format, + } + + err := s.put(req, "apis", id, "metadata", metadataKey) + if err != nil { + switch v := err.(type) { + case RequestError: + if v.HttpStatus == 404 { + return nil + } + } + + return err + } + + return nil +} + +// DeployAPI deploys the current configuration of the API to the gateway instances. +func (s *GraviteeSession) DeployAPI(id string) error { + err := s.post("", "apis", id, "deploy") + if err != nil { + return err + } + + return nil +} diff --git a/vendor/github.com/konsorten/go-gravitee/gravitee_apis_test.go b/vendor/github.com/konsorten/go-gravitee/gravitee_apis_test.go new file mode 100644 index 0000000..92d10c3 --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/gravitee_apis_test.go @@ -0,0 +1,168 @@ +package gravitee + +import ( + "fmt" + "math/rand" + "testing" +) + +func TestGetAllAPIs(t *testing.T) { + session := createTestSession(t) + + apis, err := session.GetAllAPIs() + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", apis) +} + +func TestGetAPIsByLabel(t *testing.T) { + session := createTestSession(t) + + apis, err := session.GetAPIsByLabel("gravitee-go") + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", apis) + + api, err := session.GetAPI(apis[0].ID) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", api) + + meta, err := session.GetAPIMetadata(apis[0].ID) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", meta) + + for _, m := range meta { + if m.IsLocal() { + md, err := session.GetLocalAPIMetadata(apis[0].ID, m.Key) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", md) + } + } +} + +func TestSetLocalAPIMetadata(t *testing.T) { + session := createTestSession(t) + + apis, err := session.GetAPIsByLabel("gravitee-go") + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", apis) + + key := fmt.Sprintf("unittest-%v", rand.Int63()) + val := fmt.Sprintf("vvvv#%v", rand.Int63()) + err = session.SetLocalAPIMetadata(apis[0].ID, key, val, ApiMetadataFormat_String) + if err != nil { + t.Fatal(err) + } + + md, err := session.GetLocalAPIMetadata(apis[0].ID, key) + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", md) + + if md.Value() != val { + t.Fatal("Expected local value") + } + + err = session.UnsetLocalAPIMetadata(apis[0].ID, key) + if err != nil { + t.Fatal(err) + } + + notFound, err := session.GetLocalAPIMetadata(apis[0].ID, key) + if err != nil { + t.Fatal(err) + } + if notFound != nil { + t.Fatal("Expected not found") + } +} + +func TestGetLocalAPIMetadataNotFound(t *testing.T) { + session := createTestSession(t) + + apis, err := session.GetAPIsByLabel("gravitee-go") + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", apis) + + notFound, err := session.GetLocalAPIMetadata(apis[0].ID, "d0esn0tex1st") + if err != nil { + t.Fatal(err) + } + if notFound != nil { + t.Fatal("Expected not found") + } +} + +func TestUnsetLocalAPIMetadataNotFound(t *testing.T) { + session := createTestSession(t) + + apis, err := session.GetAPIsByLabel("gravitee-go") + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", apis) + + err = session.UnsetLocalAPIMetadata(apis[0].ID, "d0esn0tex1st") + if err != nil { + t.Fatal(err) + } +} + +func TestDeployAPI(t *testing.T) { + session := createTestSession(t) + + apis, err := session.GetAPIsByLabel("gravitee-go") + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", apis) + + err = session.DeployAPI(apis[0].ID) + if err != nil { + t.Fatal(err) + } +} + +func TestAddOrUpdateEndpoints(t *testing.T) { + session := createTestSession(t) + + apis, err := session.GetAPIsByLabel("gravitee-go") + if err != nil { + t.Fatal(err) + } + + fmt.Printf("%v\n", apis) + + ep := []ApiDetailsEndpoint{ + MakeApiDetailsEndpoint("default", "http://klihgukjdvbfjgbjhdfb"), + MakeApiDetailsEndpoint("default2", "http://skjfgjshdfshjdgfjhsgdhj"), + } + + err = session.AddOrUpdateEndpoints(apis[0].ID, ep, true) + if err != nil { + t.Fatal(err) + } +} diff --git a/vendor/github.com/konsorten/go-gravitee/gravitee_system.go b/vendor/github.com/konsorten/go-gravitee/gravitee_system.go new file mode 100644 index 0000000..f1bb16d --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/gravitee_system.go @@ -0,0 +1,22 @@ +package gravitee + +type UserReference struct { + Id string `json:"id"` + DisplayName string `json:"displayName"` +} + +type configTenantsResponse struct { +} + +// Ping checks if the API is available. +func (s *GraviteeSession) Ping() (bool, error) { + var result *[]configTenantsResponse + + err := s.getForEntity(&result, "configuration", "tenants") + + if err != nil { + return false, err + } + + return true, nil +} diff --git a/vendor/github.com/konsorten/go-gravitee/gravitee_system_test.go b/vendor/github.com/konsorten/go-gravitee/gravitee_system_test.go new file mode 100644 index 0000000..db7765d --- /dev/null +++ b/vendor/github.com/konsorten/go-gravitee/gravitee_system_test.go @@ -0,0 +1,68 @@ +package gravitee + +import ( + "os" + "strings" + "testing" +) + +func createTestSession(t *testing.T) *GraviteeSession { + return createTestSessionEx(t, "", "") +} + +func createTestSessionEx(t *testing.T, username, password string) *GraviteeSession { + // retrieve api auth if undefined + if username == "" { + username = os.Getenv("GRAVITEE_USER") + + if username == "" { + username = "admin" + } + } + + if password == "" { + password = os.Getenv("GRAVITEE_PWD") + + if password == "" { + password = "admin" + } + } + + // retrieve hostname + host := os.Getenv("GRAVITEE_HOSTNAME") + + if host == "" { + host = "api.konsorten-api.de" + } + + // create the session + session, err := Connect(host, username, password, nil) + + if err != nil { + t.Fatalf("Failed to connect to Gravitee Management API: %v", err) + } + + return session +} + +func TestInvalidHost(t *testing.T) { + _, err := Connect("d0esn0tex1st", "inva1dus3r", "inval1dAp1K3y", nil) + + if err == nil { + t.Fatal("Error expected") + } + + if !strings.Contains(err.Error(), "no such host") { + t.Fatalf("Wrong error message returned: %v", err) + } +} + +func TestPing(t *testing.T) { + session := createTestSession(t) + + success, msg := session.Ping() + + if !success { + t.Fatalf("Ping failed: %v", msg) + } +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/.gitignore b/vendor/github.com/konsorten/zevenet-lb-go/.gitignore new file mode 100644 index 0000000..d3f5a12 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/.gitignore @@ -0,0 +1 @@ + diff --git a/vendor/github.com/konsorten/zevenet-lb-go/.vscode/extensions.json b/vendor/github.com/konsorten/zevenet-lb-go/.vscode/extensions.json new file mode 100644 index 0000000..18216d0 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/.vscode/extensions.json @@ -0,0 +1,11 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + "heaths.vscode-guid", // Insert GUID + "streetsidesoftware.code-spell-checker", // Spelling and Grammar Checker + "lukehoban.go" // Go language + ] +} \ No newline at end of file diff --git a/vendor/github.com/konsorten/zevenet-lb-go/.vscode/settings.json b/vendor/github.com/konsorten/zevenet-lb-go/.vscode/settings.json new file mode 100644 index 0000000..2b97f55 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": [ + "Zapi", + "Zevenet", + "zevenetlb" + ] +} \ No newline at end of file diff --git a/vendor/github.com/konsorten/zevenet-lb-go/LICENSE b/vendor/github.com/konsorten/zevenet-lb-go/LICENSE new file mode 100644 index 0000000..36fc2df --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/LICENSE @@ -0,0 +1,9 @@ +(The MIT License) + +Copyright (c) 2018 marvin + konsorten GmbH (info@konsorten.de) + +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. diff --git a/vendor/github.com/konsorten/zevenet-lb-go/README.md b/vendor/github.com/konsorten/zevenet-lb-go/README.md new file mode 100644 index 0000000..db0d508 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/README.md @@ -0,0 +1,51 @@ +# Zevenet Loadbalancer for Go + +This library provides access to the Zevenet loadbalancer using its ZAPI (REST API) of v3.1. + +It is tested using Zevenet Community Edition 5.0 but should probably work with the Enterprise Edition, too. + +**[Click here to open the GoDoc documentation.](https://godoc.org/github.com/konsorten/zevenet-lb-go)** + +## Usage + +To use the API, simply create a new session: + +```go +import zevenet "github.com/konsorten/zevenet-lb-go" + +func main() { + session, _ := zevenet.Connect("myloadbalancer:444", "zapi-key", nil) + + version, _ := session.GetSystemVersion() +} +``` + +## ZAPI Key + +The API key for the Zevenet CE API can be retrieved using the web interface: + +https://myloadbalancer:444/#/system/users/zapi + +If the key is empty, click the *Generate Random Key* button and *Apply*. + +## Authors + +The library is sponsored by the [marvin + konsorten GmbH](http://www.konsorten.de). + +It is loosely based on the [go-bigip package](https://github.com/scottdware/go-bigip) by Scott Ware (and others). + +We thank all the authors who provided code to this library: + +* Felix Kollmann + +## License + +(The MIT License) + +Copyright (c) 2018 marvin + konsorten GmbH (info@konsorten.de) + +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. diff --git a/vendor/github.com/konsorten/zevenet-lb-go/examples_test.go b/vendor/github.com/konsorten/zevenet-lb-go/examples_test.go new file mode 100644 index 0000000..7b2dc9d --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/examples_test.go @@ -0,0 +1,41 @@ +package zevenetlb + +import "fmt" + +// This is how to connect to the Zevenet loadbalancer. +func ExampleConnect() { + session, _ := Connect("myloadbalancer:444", "zapi-key", nil) + + version, _ := session.GetSystemVersion() + + fmt.Println(version) +} + +// This is how to retrieve a specific farm. In this case the first farm that exists. +func ExampleZapiSession_GetFarm() { + session, _ := Connect("myloadbalancer:444", "zapi-key", nil) + + farms, _ := session.GetAllFarms() + + farm, _ := session.GetFarm(farms[0].FarmName) + + fmt.Println(farm) +} + +// This is how to create a new HTTP farm *without* SSL support. +func ExampleZapiSession_CreateFarmAsHTTP() { + session, _ := Connect("myloadbalancer:444", "zapi-key", nil) + + farm, _ := session.CreateFarmAsHTTP("mynewfarm", "10.10.10.10", 80) + + fmt.Println(farm) +} + +// This is how to create a new HTTP farm *with* SSL support, using the Zevenet default certificate. +func ExampleZapiSession_CreateFarmAsHTTPS() { + session, _ := Connect("myloadbalancer:444", "zapi-key", nil) + + farm, _ := session.CreateFarmAsHTTPS("mynewfarm", "10.10.10.10", 443, "zencert.pem") + + fmt.Println(farm) +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/godoc.go b/vendor/github.com/konsorten/zevenet-lb-go/godoc.go new file mode 100644 index 0000000..be6cf32 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/godoc.go @@ -0,0 +1,6 @@ +// Package zevenetlb provides access to the Zevenet loadbalancer using its ZAPI (REST API) of v3.1. +// +// It is tested using Zevenet Community Edition 5.0 but should probably work with the Enterprise Edition, too. +// +// More information is available on the project website: https://github.com/konsorten/zevenet-lb-go/ +package zevenetlb diff --git a/vendor/github.com/konsorten/zevenet-lb-go/zapi.go b/vendor/github.com/konsorten/zevenet-lb-go/zapi.go new file mode 100644 index 0000000..1d32662 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/zapi.go @@ -0,0 +1,317 @@ +package zevenetlb + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "strings" + "time" +) + +var defaultConfigOptions = &ConfigOptions{ + APICallTimeout: 60 * time.Second, + ZapiVersion: "3.1", +} + +// ConfigOptions contains some advanced settings on server communication. +type ConfigOptions struct { + APICallTimeout time.Duration + ZapiVersion string +} + +// ZapiSession is a container for our session state. +type ZapiSession struct { + Host string + ZapiKey string + Transport *http.Transport + ConfigOptions *ConfigOptions +} + +// String returns the session's hostname. +func (s *ZapiSession) String() string { + return s.Host +} + +// APIRequest builds our request before sending it to the server. +type APIRequest struct { + Method string + URL string + Body string + ContentType string +} + +// RequestError contains information about any error we get from a request. +type RequestError struct { + Message string `json:"message,omitempty"` + Description string `json:"description,omitempty"` +} + +// Error returns the error message. +func (r *RequestError) Error() error { + if r.Description != "" { + return fmt.Errorf("%v failed: %v", r.Description, r.Message) + } + + return fmt.Errorf("%v", r.Message) +} + +// Connect sets up our connection to the Zevenet system. +func Connect(host, zapiKey string, configOptions *ConfigOptions) (*ZapiSession, error) { + var url string + if !strings.HasPrefix(host, "http") { + url = fmt.Sprintf("https://%s", host) + } else { + url = host + } + if configOptions == nil { + configOptions = defaultConfigOptions + } + + // create the session + session := &ZapiSession{ + Host: url, + ZapiKey: zapiKey, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + ConfigOptions: configOptions, + } + + // initialize the session + err := session.initialize() + + if err != nil { + return nil, err + } + + // done + return session, nil +} + +func (s *ZapiSession) initialize() (err error) { + // test connection + _, err = s.GetSystemVersion() + return +} + +// Ping checks if the loadbalancer is available. +func (s *ZapiSession) Ping() (bool, string) { + // test connection + _, err := s.GetSystemVersion() + + if err != nil { + return false, err.Error() + } + + return true, "" +} + +// apiCall is used to query the ZAPI. +func (s *ZapiSession) apiCall(options *APIRequest) ([]byte, error) { + var req *http.Request + client := &http.Client{ + Transport: s.Transport, + Timeout: s.ConfigOptions.APICallTimeout, + } + url := fmt.Sprintf("%v/zapi/v%v/zapi.cgi/%v", s.Host, s.ConfigOptions.ZapiVersion, options.URL) + body := bytes.NewReader([]byte(options.Body)) + req, _ = http.NewRequest(strings.ToUpper(options.Method), url, body) + + req.Header.Set("ZAPI_KEY", s.ZapiKey) + + // fmt.Println("REQ -- ", options.Method, " ", url, " -- ", options.Body) + + if len(options.ContentType) > 0 { + req.Header.Set("Content-Type", options.ContentType) + } + + res, err := client.Do(req) + if err != nil { + return nil, err + } + + defer res.Body.Close() + + data, _ := ioutil.ReadAll(res.Body) + + if res.StatusCode >= 400 { + if res.Header["Content-Type"][0] == "application/json" { + return data, s.checkError(data) + } + + return data, fmt.Errorf("HTTP %d :: %s", res.StatusCode, string(data[:])) + } + + // fmt.Println("Resp --", res.StatusCode, " -- ", string(data)) + return data, nil +} + +func (s *ZapiSession) iControlPath(parts []string) string { + var buffer bytes.Buffer + for i, p := range parts { + buffer.WriteString(strings.Replace(p, "/", "~", -1)) + if i < len(parts)-1 { + buffer.WriteString("/") + } + } + return buffer.String() +} + +//Generic delete +func (s *ZapiSession) delete(path ...string) error { + req := &APIRequest{ + Method: "delete", + URL: s.iControlPath(path), + } + + _, callErr := s.apiCall(req) + return callErr +} + +func (s *ZapiSession) post(body interface{}, path ...string) error { + marshalJSON, err := jsonMarshal(body) + if err != nil { + return err + } + + req := &APIRequest{ + Method: "post", + URL: s.iControlPath(path), + Body: strings.TrimRight(string(marshalJSON), "\n"), + ContentType: "application/json", + } + + _, callErr := s.apiCall(req) + return callErr +} + +func (s *ZapiSession) put(body interface{}, path ...string) error { + marshalJSON, err := jsonMarshal(body) + if err != nil { + return err + } + + req := &APIRequest{ + Method: "put", + URL: s.iControlPath(path), + Body: strings.TrimRight(string(marshalJSON), "\n"), + ContentType: "application/json", + } + + _, callErr := s.apiCall(req) + return callErr +} + +//Get a url and populate an entity. If the entity does not exist (404) then the +//passed entity will be untouched and false will be returned as the second parameter. +//You can use this to distinguish between a missing entity or an actual error. +func (s *ZapiSession) getForEntity(e interface{}, path ...string) error { + req := &APIRequest{ + Method: "get", + URL: s.iControlPath(path), + ContentType: "application/json", + } + + resp, err := s.apiCall(req) + if err != nil { + var reqError RequestError + json.Unmarshal(resp, &reqError) + return err + } + + err = json.Unmarshal(resp, e) + if err != nil { + return err + } + + return nil +} + +// checkError handles any errors we get from our API requests. It returns either the +// message of the error, if any, or nil. +func (s *ZapiSession) checkError(resp []byte) error { + if len(resp) == 0 { + return nil + } + + var reqError RequestError + + err := json.Unmarshal(resp, &reqError) + if err != nil { + return fmt.Errorf("%s\n%s", err.Error(), string(resp[:])) + } + + err = reqError.Error() + if err != nil { + return err + } + + return nil +} + +// jsonMarshal specifies an encoder with 'SetEscapeHTML' set to 'false' so that <, >, and & are not escaped. https://golang.org/pkg/encoding/json/#Marshal +// https://stackoverflow.com/questions/28595664/how-to-stop-json-marshal-from-escaping-and +func jsonMarshal(t interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + encoder := json.NewEncoder(buffer) + encoder.SetEscapeHTML(false) + err := encoder.Encode(t) + return buffer.Bytes(), err +} + +// Helper to copy between transfer objects and model objects to hide the myriad of boolean representations +// in the iControlREST api. DTO fields can be tagged with bool:"yes|enabled|true" to set what true and false +// marshal to. +func marshal(to, from interface{}) error { + toVal := reflect.ValueOf(to).Elem() + fromVal := reflect.ValueOf(from).Elem() + toType := toVal.Type() + for i := 0; i < toVal.NumField(); i++ { + toField := toVal.Field(i) + toFieldType := toType.Field(i) + fromField := fromVal.FieldByName(toFieldType.Name) + if fromField.Interface() != nil && fromField.Kind() == toField.Kind() { + toField.Set(fromField) + } else if toField.Kind() == reflect.Bool && fromField.Kind() == reflect.String { + switch fromField.Interface() { + case "yes", "enabled", "true": + toField.SetBool(true) + break + case "no", "disabled", "false", "": + toField.SetBool(false) + break + default: + return fmt.Errorf("Unknown boolean conversion for %s: %s", toFieldType.Name, fromField.Interface()) + } + } else if fromField.Kind() == reflect.Bool && toField.Kind() == reflect.String { + tag := toFieldType.Tag.Get("bool") + switch tag { + case "yes": + toField.SetString(toBoolString(fromField.Interface().(bool), "yes", "no")) + break + case "enabled": + toField.SetString(toBoolString(fromField.Interface().(bool), "enabled", "disabled")) + break + case "true": + toField.SetString(toBoolString(fromField.Interface().(bool), "true", "false")) + break + } + } else { + return fmt.Errorf("Unknown type conversion %s -> %s", fromField.Kind(), toField.Kind()) + } + } + return nil +} + +func toBoolString(b bool, trueStr, falseStr string) string { + if b { + return trueStr + } + return falseStr +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/zapi_certs.go b/vendor/github.com/konsorten/zevenet-lb-go/zapi_certs.go new file mode 100644 index 0000000..0cbf529 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/zapi_certs.go @@ -0,0 +1,37 @@ +package zevenetlb + +import "fmt" + +type certListResponse struct { + Description string `json:"description"` + Params []CertificateDetails `json:"params"` +} + +// CertificateDetails contains the details on a certificate. +// See https://www.zevenet.com/zapidoc_ce_v3.1/#list-all-certificates +type CertificateDetails struct { + CommonName string `json:"CN"` + CreationDate string `json:"creation"` + ExpirationDate string `json:"expiration"` + Filename string `json:"file"` + Issuer string `json:"issuer"` + Type string `json:"type"` +} + +// String returns the certificates common name and filename. +func (sv CertificateDetails) String() string { + return fmt.Sprintf("%v (%v)", sv.CommonName, sv.Filename) +} + +// GetAllCertificates returns list of all available certificates and CSRs. +func (s *ZapiSession) GetAllCertificates() ([]CertificateDetails, error) { + var result *certListResponse + + err := s.getForEntity(&result, "certificates") + + if err != nil { + return nil, err + } + + return result.Params, nil +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/zapi_certs_test.go b/vendor/github.com/konsorten/zevenet-lb-go/zapi_certs_test.go new file mode 100644 index 0000000..a042cef --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/zapi_certs_test.go @@ -0,0 +1,25 @@ +package zevenetlb + +import ( + "testing" +) + +func TestGetAllCertificates(t *testing.T) { + session := createTestSession(t) + + res, err := session.GetAllCertificates() + + if err != nil { + t.Fatal(err) + } + + if len(res) <= 0 { + t.Fatal("No certificates returned") + } + + //t.Logf("Certificates: %v", res) + + for _, c := range res { + t.Logf("Certificate: %v", c) + } +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/zapi_farms.go b/vendor/github.com/konsorten/zevenet-lb-go/zapi_farms.go new file mode 100644 index 0000000..363e773 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/zapi_farms.go @@ -0,0 +1,687 @@ +package zevenetlb + +import ( + "fmt" + "strconv" + "strings" +) + +// OptionalBool represents an optional boolean having an *undefined/nil* state. +type OptionalBool string + +const ( + OptionalBool_Nil OptionalBool = "" + OptionalBool_True OptionalBool = "true" + OptionalBool_False OptionalBool = "false" +) + +type genericResponse struct { + Description string `json:"description"` +} + +// FarmProfile is an enumeration of possible farm profiles. +type FarmProfile string + +const ( + FarmProfile_HTTP FarmProfile = "http" + FarmProfile_HTTPS FarmProfile = "https" + FarmProfile_Level4NAT FarmProfile = "l4xnat" + FarmProfile_DataLink FarmProfile = "datalink" +) + +type farmListResponse struct { + Description string `json:"description"` + Params []FarmInfo `json:"params"` +} + +// FarmInfo contains the list of all available farms. +// See https://www.zevenet.com/zapidoc_ce_v3.1/#list-all-farms +type FarmInfo struct { + FarmName string `json:"farmname"` + Profile FarmProfile `json:"profile"` + Status FarmStatus `json:"status"` + VirtualIP string `json:"vip"` + VirtualPort int `json:"vport,string"` +} + +// String returns the farm's name and profile. +func (fi *FarmInfo) String() string { + return fmt.Sprintf("%v (%v)", fi.FarmName, fi.Profile) +} + +// GetAllFarms returns list os all available farms. +func (s *ZapiSession) GetAllFarms() ([]FarmInfo, error) { + var result *farmListResponse + + err := s.getForEntity(&result, "farms") + + if err != nil { + return nil, err + } + + return result.Params, nil +} + +type farmDetailsResponse struct { + Description string `json:"description"` + Params FarmDetails `json:"params"` + Services []ServiceDetails `json:"services"` +} + +// FarmCiphers is an enumeration of possible selections of *Ciphers* to be used for an https listener. +// The custom cipher requires *CiphersCustom* to bet set to an OpenSSL compatible ciphers string. +type FarmCiphers string + +const ( + FarmCiphers_All FarmCiphers = "all" + FarmCiphers_High FarmCiphers = "highsecurity" + FarmCiphers_Custom FarmCiphers = "customsecurity" +) + +// FarmHTTPVerb is an enumeration of possible *HTTPVerbs* to be used for an http/s listener. +type FarmHTTPVerb string + +const ( + // FarmHTTPVerb_Standard accepts GET, POST, HEAD + FarmHTTPVerb_Standard FarmHTTPVerb = "standardHTTP" + + // FarmHTTPVerb_Extended accepts GET, POST, HEAD, PUT, DELETE + FarmHTTPVerb_Extended FarmHTTPVerb = "extendedHTTP" + + // FarmHTTPVerb_WebDAV accepts GET, POST, HEAD, PUT, DELETE, LOCK, UNLOCK, PROPFIND, PROPPATCH, SEARCH, MKCOL, MOVE, COPY, OPTIONS, TRACE, MKACTIVITY, CHECKOUT, MERGE, REPORT + FarmHTTPVerb_WebDAV FarmHTTPVerb = "standardWebDAV" + + // FarmHTTPVerb_MicrosoftWebDAV accepts GET, POST, HEAD, PUT, DELETE, LOCK, UNLOCK, PROPFIND, PROPPATCH, SEARCH, MKCOL, MOVE, COPY, OPTIONS, TRACE, MKACTIVITY, CHECKOUT, MERGE, REPORT, SUBSCRIBE, UNSUBSCRIBE, NOTIFY, BPROPFIND, BPROPPATCH, POLL, BMOVE, BCOPY, BDELETE, CONNECT + FarmHTTPVerb_MicrosoftWebDAV FarmHTTPVerb = "MSextWebDAV" + + // FarmHTTPVerb_MicrosoftRPC accepts GET, POST, HEAD, PUT, DELETE, LOCK, UNLOCK, PROPFIND, PROPPATCH, SEARCH, MKCOL, MOVE, COPY, OPTIONS, TRACE, MKACTIVITY, CHECKOUT, MERGE, REPORT, SUBSCRIBE, UNSUBSCRIBE, NOTIFY, BPROPFIND, BPROPPATCH, POLL, BMOVE, BCOPY, BDELETE, CONNECT, RPC_IN_DATA, RPC_OUT_DATA + FarmHTTPVerb_MicrosoftRPC FarmHTTPVerb = "MSRPCext" +) + +// FarmListener is an enumeration of possible selections of *Listener* values. +type FarmListener string + +const ( + FarmListener_HTTP FarmListener = "http" + FarmListener_HTTPS FarmListener = "https" +) + +// FarmRewriteLocation is an enumeration of possible selections of *RewriteLocation* values. +// If it is enabled, the farm is forced to modify the Location: and Content-location: headers in responses to clients with the virtual host. +type FarmRewriteLocation string + +const ( + FarmRewriteLocation_Enabled FarmRewriteLocation = "enabled" + FarmRewriteLocation_BackendsOnly FarmRewriteLocation = "enabled-backends" + FarmRewriteLocation_Disabled FarmRewriteLocation = "disabled" +) + +// FarmStatus is an enumeration of possible selections of *Status* values. +type FarmStatus string + +const ( + // FarmStatus_Up means the farm is up and all the backends are working fine. + FarmStatus_Up FarmStatus = "up" + + // FarmStatus_Down means the farm is not running. Use *StartFarm()* to start the farm. + FarmStatus_Down FarmStatus = "down" + + // FarmStatus_NeedsRestart means the farm is up but it is pending of a restart action. Use *RestartFarm()* to perform the required restart. + FarmStatus_NeedsRestart FarmStatus = "needed restart" + + // FarmStatus_Critical means the farm is up and all backends are unreachable or maintenance. This is usually the case for newly created empty farms. + FarmStatus_Critical FarmStatus = "critical" + + // FarmStatus_Problem means the farm is up and there are some backend unreachable, but at least one backend is in up status. + FarmStatus_Problem FarmStatus = "problem" + + // FarmStatus_Maintenance means the farm is up and there are backends in up status, but at least one backend is in maintenance mode. Use *SetServiceMaintenance(false)* on the service. + FarmStatus_Maintenance FarmStatus = "maintenance" +) + +// FarmDetails contains all information regarding a farm and the services. +// See https://www.zevenet.com/zapidoc_ce_v3.1/#retrieve-farm-by-name +type FarmDetails struct { + Certificates []CertificateInfo `json:"certlist"` + FarmName string `json:"farmname"` + CiphersCustom string `json:"cipherc,omitempty"` + Ciphers FarmCiphers `json:"ciphers,omitempty"` + ConnectionTimeoutSeconds int `json:"contimeout"` + DisableSSLv2 bool `json:"disable_sslv2,string"` + DisableSSLv3 bool `json:"disable_sslv3,string"` + DisableTLSv1 bool `json:"disable_tlsv1,string"` + DisableTLSv11 bool `json:"disable_tlsv1_1,string"` + DisableTLSv12 bool `json:"disable_tlsv1_2,string"` + ErrorString414 string `json:"error414"` + ErrorString500 string `json:"error500"` + ErrorString501 string `json:"error501"` + ErrorString503 string `json:"error503"` + HTTPVerbs FarmHTTPVerb `json:"httpverb"` + Listener FarmListener `json:"listener"` + RequestTimeoutSeconds int `json:"reqtimeout"` + ResponseTimeoutSeconds int `json:"restimeout"` + ResurrectIntervalSeconds int `json:"resurrectime"` + RewriteLocation FarmRewriteLocation `json:"rewritelocation"` + Status FarmStatus `json:"status"` + VirtualIP string `json:"vip"` + VirtualPort int `json:"vport"` + Services []ServiceDetails `json:"services"` +} + +// String returns the farm's name and listener. +func (fd *FarmDetails) String() string { + return fmt.Sprintf("%v (%v)", fd.FarmName, fd.Listener) +} + +// IsHTTP checks if the farm has HTTP or HTTPS support enabled. +func (fd *FarmDetails) IsHTTP() bool { + return strings.HasPrefix(string(fd.Listener), "http") +} + +// IsRunning checks if the farm is up and running. +func (fd *FarmDetails) IsRunning() bool { + return fd.Status == FarmStatus_Up +} + +// GetService retrieves a service by its name, or returns *nil* if not found. +func (fd *FarmDetails) GetService(serviceName string) (*ServiceDetails, error) { + for _, s := range fd.Services { + if s.ServiceName == serviceName { + return &s, nil + } + } + + return nil, nil +} + +// GetFarm returns details on a specific farm. +func (s *ZapiSession) GetFarm(farmName string) (*FarmDetails, error) { + var result *farmDetailsResponse + + err := s.getForEntity(&result, "farms", farmName) + + if err != nil { + // farm not found? + if strings.Contains(err.Error(), "Farm not found") { + return nil, nil + } + + return nil, err + } + + // inject values + if result != nil { + result.Params.FarmName = farmName + result.Params.Services = result.Services + + for s := range result.Params.Services { + service := &result.Params.Services[s] + + service.FarmName = farmName + + for b := range service.Backends { + backend := &service.Backends[b] + + backend.FarmName = farmName + backend.ServiceName = service.ServiceName + } + } + } + + return &result.Params, nil +} + +// DeleteFarm will delete an existing farm (or do nothing if missing) +func (s *ZapiSession) DeleteFarm(farmName string) (bool, error) { + // retrieve farm details + farm, err := s.GetFarm(farmName) + + if err != nil { + return false, err + } + + // farm does not exist? + if farm == nil { + return false, nil + } + + // delete the farm + return true, s.delete("farms", farmName) +} + +type farmCreate struct { + FarmName string `json:"farmname"` + Profile string `json:"profile"` + VirtualIP string `json:"vip"` + VirtualPort int `json:"vport"` +} + +// CreateFarmAsHTTP creates a new HTTP farm. +// A newly created farm is in the *critical* state, due to the lack of services and backends. +// The *virtualPort* is optional and can be 0, using port 80 as default. +func (s *ZapiSession) CreateFarmAsHTTP(farmName string, virtualIP string, virtualPort int) (*FarmDetails, error) { + // set default HTTP port + if virtualPort <= 0 { + virtualPort = 80 + } + + // create the farm + req := farmCreate{ + FarmName: farmName, + Profile: "http", + VirtualIP: virtualIP, + VirtualPort: virtualPort, + } + + err := s.post(req, "farms") + + if err != nil { + return nil, err + } + + // retrieve status + return s.GetFarm(farmName) +} + +// CreateFarmAsHTTPS creates a new HTTPS farm. +// A newly created farm is in the *critical* state, due to the lack of services and backends. +// The *virtualPort* is optional and can be 0, using port 443 as default. +func (s *ZapiSession) CreateFarmAsHTTPS(farmName string, virtualIP string, virtualPort int, certFilename string) (*FarmDetails, error) { + // set default HTTPS port + if virtualPort <= 0 { + virtualPort = 443 + } + + // create the farm + farm, err := s.CreateFarmAsHTTP(farmName, virtualIP, virtualPort) + + if err != nil { + return nil, err + } + + // update the farm + farm.Listener = "https" + farm.Ciphers = "highsecurity" + farm.DisableSSLv2 = false + farm.DisableSSLv3 = false + farm.DisableTLSv1 = false + + s.UpdateFarm(farm) + + return farm, nil +} + +// UpdateFarm updates the HTTP/S farm. +// This method does *not* update the *services*. Use *UpdateService()* instead. +func (s *ZapiSession) UpdateFarm(farm *FarmDetails) error { + return s.put(farm, "farms", farm.FarmName) +} + +type farmAction struct { + Action string `json:"action"` +} + +// StartFarm will start a stopped farm. +func (s *ZapiSession) StartFarm(farmName string) error { + req := farmAction{Action: "start"} + + return s.put(req, "farms", farmName, "actions") +} + +// StopFarm will stop a running farm. +func (s *ZapiSession) StopFarm(farmName string) error { + req := farmAction{Action: "stop"} + + return s.put(req, "farms", farmName, "actions") +} + +// RestartFarm will restart a running farm. +func (s *ZapiSession) RestartFarm(farmName string) error { + req := farmAction{Action: "restart"} + + return s.put(req, "farms", farmName, "actions") +} + +// CertificateInfo contains reference information on a certificate. +type CertificateInfo struct { + Filename string `json:"file"` + ID int `json:"id"` +} + +// String returns the certificate's filename. +func (ci CertificateInfo) String() string { + return ci.Filename +} + +type serviceDetailsResponse struct { + Description string `json:"description"` + Params ServiceDetails `json:"params"` +} + +// ServiceRedirectType is an enumeration of possible selections of *RedirectType* values. +type ServiceRedirectType string + +const ( + // ServiceRedirectType_Default means the url is taken as an absolute host and path to redirect to. + ServiceRedirectType_Default ServiceRedirectType = "default" + + // ServiceRedirectType_Append means the original request path or URI will be appended to the host and path. + ServiceRedirectType_Append ServiceRedirectType = "append" + + // ServiceRedirectType_Disabled means the *RedirectURL* field is not set. + ServiceRedirectType_Disabled ServiceRedirectType = "" +) + +// ServiceConnPersistenceMode is an enumeration of possible selections of *ConnectionPersistenceMode* values. +type ServiceConnPersistenceMode string + +const ( + // ServiceConnPersistenceMode_Disabled means no action is taken. + ServiceConnPersistenceMode_Disabled ServiceConnPersistenceMode = "" + + // ServiceConnPersistenceMode_IPAddress means the persistence session is done in base of client IP. + ServiceConnPersistenceMode_IPAddress ServiceConnPersistenceMode = "IP" + + // ServiceConnPersistenceMode_BasicHeaders means the persistence session is done in base of BASIC headers. + ServiceConnPersistenceMode_BasicHeaders ServiceConnPersistenceMode = "BASIC" + + // ServiceConnPersistenceMode_Url means the persistence session is done in base of a field in the URI. Set the query parameter name in *ConnectionPersistenceID*. + ServiceConnPersistenceMode_Url ServiceConnPersistenceMode = "URL" + + // ServiceConnPersistenceMode_QueryParameter means the persistence session is done in base of a value at the end of the URI. + ServiceConnPersistenceMode_QueryParameter ServiceConnPersistenceMode = "PARM" + + // ServiceConnPersistenceMode_Cookie means the persistence session is done in base of a cookie name, this cookie has to be created by the backends! Set the cookie name in *ConnectionPersistenceID*. + ServiceConnPersistenceMode_Cookie ServiceConnPersistenceMode = "COOKIE" + + // ServiceConnPersistenceMode_Header means the persistence session is done in base of a Header name. Set the header name in *ConnectionPersistenceID*. + ServiceConnPersistenceMode_Header ServiceConnPersistenceMode = "HEADER" +) + +// ServiceDetails contains all information regarding a single service. +type ServiceDetails struct { + ServiceName string `json:"id"` + FarmGuardianEnabled bool `json:"fgenabled,string"` + FarmGuardianLogsEnabled OptionalBool `json:"fglog"` + FarmGuardianScript string `json:"fgscript"` + FarmGuardianCheckIntervalSeconds int `json:"fgtimecheck"` + EncryptedBackends bool `json:"httpsb,string"` + LastResponseBalancingEnabled bool `json:"leastresp,string"` + ConnectionPersistenceMode ServiceConnPersistenceMode `json:"persistence"` + ConnectionPersistenceID string `json:"sessionid"` + ConnectionPersistenceTimeoutSeconds int `json:"ttl"` + RedirectURL string `json:"redirect"` + RedirectType ServiceRedirectType `json:"redirecttype"` + URLPattern string `json:"urlp"` + HostPattern string `json:"vhost"` + Backends []BackendDetails `json:"backends"` + FarmName string `json:"farmname"` +} + +// String returns the services' name. +func (sd ServiceDetails) String() string { + return sd.ServiceName +} + +// GetBackend retrieves a backend by its ID, or returns *nil* if not found. +func (sd *ServiceDetails) GetBackend(backendID int) (*BackendDetails, error) { + for _, s := range sd.Backends { + if s.ID == backendID { + return &s, nil + } + } + + return nil, nil +} + +// GetBackendByAddress retrieves a backend by its IP address and port, or returns *nil* if not found. The *port* is optional and can be 0. +func (sd *ServiceDetails) GetBackendByAddress(ipAddress string, port int) (*BackendDetails, error) { + for _, s := range sd.Backends { + if s.IPAddress == ipAddress && (port <= 0 || s.Port == port) { + return &s, nil + } + } + + return nil, nil +} + +type serviceCreate struct { + ServiceName string `json:"id"` +} + +// DeleteService will delete an existing service (or do nothing if service or farm is missing) +func (s *ZapiSession) DeleteService(farmName string, serviceName string) (bool, error) { + // retrieve farm details + farm, err := s.GetFarm(farmName) + + if err != nil { + return false, err + } + + // farm does not exist? + if farm == nil { + return false, nil + } + + // does the service exist? + service, err := farm.GetService(serviceName) + + if err != nil { + return false, err + } + + if service == nil { + return false, nil + } + + // delete the service + return true, s.delete("farms", farmName, "services", serviceName) +} + +// CreateService creates a new service on a farm. +func (s *ZapiSession) CreateService(farmName string, serviceName string) (*ServiceDetails, error) { + // create the service + req := serviceCreate{ + ServiceName: serviceName, + } + + err := s.post(req, "farms", farmName, "services") + + if err != nil { + return nil, err + } + + // retrieve status + farm, err := s.GetFarm(farmName) + + if err != nil { + return nil, err + } + + return farm.GetService(serviceName) +} + +type farmguardianUpdate struct { + ServiceName string `json:"service"` + FarmGuardianEnabled bool `json:"fgenabled,string"` + FarmGuardianLogsEnabled OptionalBool `json:"fglog"` + FarmGuardianScript string `json:"fgscript"` + FarmGuardianCheckIntervalSeconds int `json:"fgtimecheck"` +} + +// UpdateService updates a service on a farm. +// This method does *not* update the *backends*. Use *UpdateBackend()* instead. +func (s *ZapiSession) UpdateService(service *ServiceDetails) error { + err := s.put(service, "farms", service.FarmName, "services", service.ServiceName) + + if err != nil { + return err + } + + // update farm guardian + fg := farmguardianUpdate{ + ServiceName: service.ServiceName, + FarmGuardianEnabled: service.FarmGuardianEnabled, + FarmGuardianScript: service.FarmGuardianScript, + FarmGuardianCheckIntervalSeconds: service.FarmGuardianCheckIntervalSeconds, + FarmGuardianLogsEnabled: service.FarmGuardianLogsEnabled, + } + + if fg.FarmGuardianScript == "" { + fg.FarmGuardianScript = "check_http -H HOST -p PORT" + } + + return s.put(fg, "farms", service.FarmName, "fg") +} + +type backendDetailsResponse struct { + Description string `json:"description"` + Params BackendDetails `json:"params"` +} + +// BackendStatus is an enumeration of possible selections of *Status* values. +type BackendStatus string + +const ( + // BackendStatus_Up means the backend is ready to receive connections. + BackendStatus_Up BackendStatus = "up" + + // BackendStatus_Down means the backend is not working. + BackendStatus_Down BackendStatus = "down" + + // BackendStatus_Maintenance means backend is marked as not ready for receiving connections by the administrator. Use *SetServiceMaintenance(false)* on the service. + BackendStatus_Maintenance BackendStatus = "maintenance" + + // BackendStatus_Undefined means the backend status has been not checked. + BackendStatus_Undefined BackendStatus = "undefined" +) + +// BackendDetails contains all information regarding a single backend server. +type BackendDetails struct { + ID int `json:"id"` + IPAddress string `json:"ip"` + Port int `json:"port"` + Status BackendStatus `json:"status"` + TimeoutSeconds *int `json:"timeout,omitempty"` + Weight *int `json:"weight,omitempty"` + FarmName string `json:"farmname"` + ServiceName string `json:"servicename"` +} + +// String returns the backend's IP, port, ID, and status. +func (bd BackendDetails) String() string { + return fmt.Sprintf("%v:%v (ID: %v, Status: %v)", bd.IPAddress, bd.Port, bd.ID, bd.Status) +} + +type backendCreate struct { + IPAddress string `json:"ip"` + Port int `json:"port"` +} + +// DeleteBackend will delete an existing backend (or do nothing if backend or service or farm is missing) +func (s *ZapiSession) DeleteBackend(farmName string, serviceName string, backendId int) (bool, error) { + // retrieve farm details + farm, err := s.GetFarm(farmName) + + if err != nil { + return false, err + } + + // farm does not exist? + if farm == nil { + return false, nil + } + + // does the service exist? + service, err := farm.GetService(serviceName) + + if err != nil { + return false, err + } + + if service == nil { + return false, nil + } + + // does the backend exist? + backend, err := service.GetBackend(backendId) + + if err != nil { + return false, err + } + + if backend == nil { + return false, nil + } + + // delete the backend + return true, s.delete("farms", farmName, "services", serviceName, "backends", strconv.Itoa(backendId)) +} + +// CreateBackend creates a new backend on a service on a farm. +func (s *ZapiSession) CreateBackend(farmName string, serviceName string, backendIP string, backendPort int) (*BackendDetails, error) { + // create the backend + req := backendCreate{ + IPAddress: backendIP, + Port: backendPort, + } + + err := s.post(req, "farms", farmName, "services", serviceName, "backends") + + if err != nil { + return nil, err + } + + // retrieve status + farm, err := s.GetFarm(farmName) + + if err != nil { + return nil, err + } + + service, err := farm.GetService(serviceName) + + if err != nil { + return nil, err + } + + return service.GetBackendByAddress(backendIP, backendPort) +} + +// UpdateBackend updates a backend on a service on a farm. +func (s *ZapiSession) UpdateBackend(backend *BackendDetails) error { + return s.put(backend, "farms", backend.FarmName, "services", backend.ServiceName, "backends", strconv.Itoa(backend.ID)) +} + +type backendMaintenance struct { + Action string `json:"action"` + Mode string `json:"mode,omitempty"` +} + +// SetBackendMaintenance updates a backend on a service on a farm. +// To cut and disconnect any existing connections when enabling maintenance, set *cutExistingConnections* to true. +func (s *ZapiSession) SetBackendMaintenance(backend *BackendDetails, enableMaintenance bool, cutExistingConnections bool) error { + var cmd backendMaintenance + + if enableMaintenance { + var mode string + + if cutExistingConnections { + mode = "cut" + } else { + mode = "drain" + } + + cmd = backendMaintenance{ + Action: "maintenance", + Mode: mode, + } + } else { + // recover from maintenance + cmd = backendMaintenance{ + Action: "up", + } + } + + return s.put(cmd, "farms", backend.FarmName, "services", backend.ServiceName, "backends", strconv.Itoa(backend.ID), "maintenance") +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/zapi_farms_test.go b/vendor/github.com/konsorten/zevenet-lb-go/zapi_farms_test.go new file mode 100644 index 0000000..516eda0 --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/zapi_farms_test.go @@ -0,0 +1,386 @@ +package zevenetlb + +import ( + "crypto/tls" + "fmt" + "io/ioutil" + "math/rand" + "net/http" + "strings" + "testing" +) + +const ( + unitTestFarmName = "UNITTESTGO" + unitTestVirtualIP = "10.209.0.31" +) + +func webGet(url string) (string, int, error) { + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + res, err := client.Get(url) + + if err != nil { + return "", 0, err + } + + defer res.Body.Close() + + // read response body + data, err := ioutil.ReadAll(res.Body) + + if err != nil { + return "", 0, err + } + + return string(data), res.StatusCode, nil +} + +func TestGetAllFarms(t *testing.T) { + session := createTestSession(t) + + res, err := session.GetAllFarms() + + if err != nil { + t.Fatal(err) + } + + if len(res) <= 0 { + t.Fatal("No farms returned") + } + + //t.Logf("Farms: %v", res) + + // get the farm details + for _, f := range res { + farm, err := session.GetFarm(f.FarmName) + + if err != nil { + t.Fatal(err) + } + + if farm == nil { + t.Fatalf("Farm not found: %v", f.FarmName) + } + + t.Logf("Farm: %v", farm) + + for _, c := range farm.Certificates { + t.Logf(" Certificate: %v", c) + } + + for _, s := range farm.Services { + t.Logf(" Service: %v", s) + + for _, b := range s.Backends { + t.Logf(" Backend: %v", b) + } + } + } +} + +func TestRoundtripHTTPFarm(t *testing.T) { + session := createTestSession(t) + + // ensure the farm does not exist + _, err := session.DeleteFarm(unitTestFarmName) + + if err != nil { + t.Fatal(err) + } + + // create the new farm + farm, err := session.CreateFarmAsHTTP(unitTestFarmName, unitTestVirtualIP, 0) + + if err != nil { + t.Fatal(err) + } + + defer session.DeleteFarm(farm.FarmName) + + t.Logf("New farm: %v, Status: %v", farm, farm.Status) + + // update 503 message (for testing) + farm.ErrorString503 = fmt.Sprintf("Service unavailable ## %v ##", rand.Int63()) + + err = session.UpdateFarm(farm) + + if err != nil { + t.Fatal(err) + } + + // restart the farm + err = session.RestartFarm(farm.FarmName) + + if err != nil { + t.Fatal(err) + } + + // try to connect + resBody, resCode, err := webGet(fmt.Sprintf("http://%v:%v", farm.VirtualIP, farm.VirtualPort)) + + if err != nil { + t.Fatal(err) + } + + // check if the return code matches + if resCode != 503 { + t.Fatalf("Expected HTTP status code 503, but got %v", resCode) + } + + // check if the message matches + if !strings.Contains(resBody, farm.ErrorString503) { + t.Fatalf("Expected the status message to contain '%v', but got '%v'", farm.ErrorString503, resBody) + } + + // add a service + service, err := session.CreateService(farm.FarmName, "service1") + + if err != nil { + t.Fatal(err) + } + + t.Logf("Service: %v", service) + + // add a backend + backend, err := session.CreateBackend(farm.FarmName, service.ServiceName, "176.58.123.25", 80) + + if err != nil { + t.Fatal(err) + } + + t.Logf("Backend: %v", backend) + + // restart the farm + err = session.RestartFarm(farm.FarmName) + + if err != nil { + t.Fatal(err) + } + + // try to connect + resBodyExpect, _, _ := webGet("http://176.58.123.25") + resBody, resCode, err = webGet(fmt.Sprintf("http://%v:%v", farm.VirtualIP, farm.VirtualPort)) + + if err != nil { + t.Fatal(err) + } + + // check if the return code matches + if resCode != 200 { + t.Fatalf("Expected HTTP status code 200, but got %v", resCode) + } + + // check if the message matches + if !strings.Contains(resBody, resBodyExpect) { + t.Fatalf("Expected the status message to contain '%v', but got '%v'", resBodyExpect, resBody) + } + + // enable maintenance + err = session.SetBackendMaintenance(backend, true, true) + + if err != nil { + t.Fatal(err) + } + + // disable maintenance + err = session.SetBackendMaintenance(backend, false, false) + + if err != nil { + t.Fatal(err) + } + + // cleaning up, delete the backend + deleted, err := session.DeleteBackend(farm.FarmName, service.ServiceName, backend.ID) + + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Fatal("Expected deleting the backend to succeed, but failed") + } + + // delete the service + deleted, err = session.DeleteService(farm.FarmName, service.ServiceName) + + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Fatal("Expected deleting the service to succeed, but failed") + } + + // done, delete the farm + deleted, err = session.DeleteFarm(farm.FarmName) + + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Fatal("Expected deleting the farm to succeed, but failed") + } +} + +func TestRoundtripHTTPSFarm(t *testing.T) { + session := createTestSession(t) + + // ensure the farm does not exist + _, err := session.DeleteFarm(unitTestFarmName) + + if err != nil { + t.Fatal(err) + } + + // retrieve certificate list + certs, err := session.GetAllCertificates() + + if err != nil { + t.Fatal(err) + } + + if len(certs) <= 0 { + t.Fatal("No certificates found on Zevenet loadbalancer") + } + + certName := certs[0].Filename + + t.Logf("Using certificate: %v", certName) + + // create the new farm + farm, err := session.CreateFarmAsHTTPS(unitTestFarmName, unitTestVirtualIP, 0, certName) + + if err != nil { + t.Fatal(err) + } + + defer session.DeleteFarm(unitTestFarmName) + + t.Logf("New farm: %v, Status: %v", farm, farm.Status) + + // update 503 message (for testing) + farm.ErrorString503 = fmt.Sprintf("Service unavailable ## %v ##", rand.Int63()) + + err = session.UpdateFarm(farm) + + if err != nil { + t.Fatal(err) + } + + // restart the farm + err = session.RestartFarm(farm.FarmName) + + if err != nil { + t.Fatal(err) + } + + // try to connect + resBody, resCode, err := webGet(fmt.Sprintf("https://%v:%v", farm.VirtualIP, farm.VirtualPort)) + + if err != nil { + t.Fatal(err) + } + + // check if the return code matches + if resCode != 503 { + t.Fatalf("Expected HTTP status code 503, but got %v", resCode) + } + + // check if the message matches + if !strings.Contains(resBody, farm.ErrorString503) { + t.Fatalf("Expected the status message to contain '%v', but got '%v'", farm.ErrorString503, resBody) + } + + // add a service + service, err := session.CreateService(farm.FarmName, "service1") + + if err != nil { + t.Fatal(err) + } + + t.Logf("Service: %v", service) + + // enable backend re-encryption + service.EncryptedBackends = true + + err = session.UpdateService(service) + + if err != nil { + t.Fatal(err) + } + + // add a backend + backend, err := session.CreateBackend(farm.FarmName, service.ServiceName, "176.58.123.25", 443) + + if err != nil { + t.Fatal(err) + } + + t.Logf("Backend: %v", backend) + + // restart the farm + err = session.RestartFarm(farm.FarmName) + + if err != nil { + t.Fatal(err) + } + + // try to connect + resBodyExpect, _, _ := webGet("http://176.58.123.25") + resBody, resCode, err = webGet(fmt.Sprintf("https://%v:%v", farm.VirtualIP, farm.VirtualPort)) + + if err != nil { + t.Fatal(err) + } + + // check if the return code matches + if resCode != 200 { + t.Fatalf("Expected HTTP status code 200, but got %v", resCode) + } + + // check if the message matches + if !strings.Contains(resBody, resBodyExpect) { + t.Fatalf("Expected the status message to contain '%v', but got '%v'", resBodyExpect, resBody) + } + + // cleaning up, delete the backend + deleted, err := session.DeleteBackend(farm.FarmName, service.ServiceName, backend.ID) + + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Fatal("Expected deleting the backend to succeed, but failed") + } + + // delete the service + deleted, err = session.DeleteService(farm.FarmName, service.ServiceName) + + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Fatal("Expected deleting the service to succeed, but failed") + } + + // done, delete the farm + deleted, err = session.DeleteFarm(farm.FarmName) + + if err != nil { + t.Fatal(err) + } + + if !deleted { + t.Fatal("Expected deleting to succeed, but failed") + } +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/zapi_system.go b/vendor/github.com/konsorten/zevenet-lb-go/zapi_system.go new file mode 100644 index 0000000..439ee4e --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/zapi_system.go @@ -0,0 +1,44 @@ +package zevenetlb + +import ( + "fmt" + "strings" +) + +type systemVersionResponse struct { + Description string `json:"description"` + Params SystemVersion `json:"params"` +} + +// SystemVersion contains information about the system version. +// See https://www.zevenet.com/zapidoc_ce_v3.1/#show-version +type SystemVersion struct { + ApplianceVersion string `json:"appliance_version"` + Hostname string `json:"hostname"` + KernelVersion string `json:"kernel_version"` + SystemDate string `json:"system_date"` + ZevenetVersion string `json:"zevenet_version"` +} + +// String returns the version number of the system, e.g. "ZCE 5 (v5.0)" +func (sv *SystemVersion) String() string { + return fmt.Sprintf("%v (v%v)", sv.ApplianceVersion, sv.ZevenetVersion) +} + +// IsCommunityEdition checks if the Zevenet loadbalancer is the Community Edition (vs Enterprise Edition) +func (sv *SystemVersion) IsCommunityEdition() bool { + return strings.HasPrefix(sv.ApplianceVersion, "ZCE") +} + +// GetSystemVersion returns system version information. +func (s *ZapiSession) GetSystemVersion() (*SystemVersion, error) { + var result *systemVersionResponse + + err := s.getForEntity(&result, "system", "version") + + if err != nil { + return nil, err + } + + return &result.Params, nil +} diff --git a/vendor/github.com/konsorten/zevenet-lb-go/zapi_system_test.go b/vendor/github.com/konsorten/zevenet-lb-go/zapi_system_test.go new file mode 100644 index 0000000..bdfc78d --- /dev/null +++ b/vendor/github.com/konsorten/zevenet-lb-go/zapi_system_test.go @@ -0,0 +1,84 @@ +package zevenetlb + +import ( + "os" + "strings" + "testing" +) + +func createTestSession(t *testing.T) *ZapiSession { + return createTestSessionEx(t, "") +} + +func createTestSessionEx(t *testing.T, apiKey string) *ZapiSession { + // retrieve api key if undefined + if apiKey == "" { + apiKey = os.Getenv("ZAPI_KEY") + + if apiKey == "" { + t.Fatal("Failed to retrieve ZAPI key from environment variable ZAPI_KEY") + } + } + + // retrieve host name + host := os.Getenv("ZAPI_HOSTNAME") + + if host == "" { + host = "lb002.konsorten.net:444" + } + + // create the session + session, err := Connect(host, apiKey, nil) + + if err != nil { + t.Fatalf("Failed to connect to Zevenet API: %v", err) + } + + return session +} + +func TestInvalidHost(t *testing.T) { + _, err := Connect("d0esn0tex1st", "inval1dAp1K3y", nil) + + if err == nil { + t.Fatal("Error expected") + } + + if !strings.Contains(err.Error(), "no such host") { + t.Fatalf("Wrong error message returned: %v", err) + } +} + +func TestPing(t *testing.T) { + session := createTestSession(t) + + success, msg := session.Ping() + + if !success { + t.Fatalf("Ping failed: %v", msg) + } +} + +func TestGetSystemVersion(t *testing.T) { + session := createTestSession(t) + + res, err := session.GetSystemVersion() + + if err != nil { + t.Fatal(err) + } + + t.Logf("Version: %v", res) +} + +func TestIsCommunityEdition(t *testing.T) { + session := createTestSession(t) + + res, err := session.GetSystemVersion() + + if err != nil { + t.Fatal(err) + } + + t.Logf("Is Community Edition: %v", res.IsCommunityEdition()) +}