<commit_before>/*******************************************************************************
*
* Copyright 2017 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You should have received a copy of the License along with this
* program. If not, you may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*******************************************************************************/

package plugins

import (
	"fmt"
	"sort"
	"strings"

	"github.com/gophercloud/gophercloud"
	"github.com/gophercloud/gophercloud/openstack"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
	"github.com/gophercloud/gophercloud/pagination"
	"github.com/sapcc/limes/pkg/limes"
	"github.com/sapcc/limes/pkg/util"
)

//use a name that's unique to github.com/gophercloud/gophercloud/openstack/imageservice/v2/images
//to ensure that goimports does not mistakenly replace it by .../compute/v2/images
var _ images.ImageVisibility

type novaPlugin struct {
	cfg             limes.ServiceConfiguration
	scrapeInstances bool
	resources       []limes.ResourceInfo
	flavors         map[string]*flavors.Flavor
	osTypeForImage  map[string]string
}

var novaDefaultResources = []limes.ResourceInfo{
	{
		Name: "cores",
		Unit: limes.UnitNone,
	},
	{
		Name: "instances",
		Unit: limes.UnitNone,
	},
	{
		Name: "ram",
		Unit: limes.UnitMebibytes,
	},
}

func init() {
	limes.RegisterQuotaPlugin(func(c limes.ServiceConfiguration, scrapeSubresources map[string]bool) limes.QuotaPlugin {
		return &novaPlugin{
			cfg:             c,
			scrapeInstances: scrapeSubresources["instances"],
			resources:       novaDefaultResources,
		}
	})
}

//Init implements the limes.QuotaPlugin interface.
func (p *novaPlugin) Init(provider *gophercloud.ProviderClient) error {
	client, err := p.Client(provider)
	if err != nil {
		return err
	}

	resources, err := listPerFlavorInstanceResources(client)
	if err != nil {
		return err
	}
	for _, resourceName := range resources {
		p.resources = append(p.resources, limes.ResourceInfo{
			Name:     resourceName,
			Category: "per_flavor",
			Unit:     limes.UnitNone,
		})
	}

	sort.Slice(p.resources, func(i, j int) bool {
		return p.resources[i].Name < p.resources[j].Name
	})
	return nil
}

//ServiceInfo implements the limes.QuotaPlugin interface.
func (p *novaPlugin) ServiceInfo() limes.ServiceInfo {
	return limes.ServiceInfo{
		Type:        "compute",
		ProductName: "nova",
		Area:        "compute",
	}
}

//Resources implements the limes.QuotaPlugin interface.
func (p *novaPlugin) Resources() []limes.ResourceInfo {
	return p.resources
}

func (p *novaPlugin) Client(provider *gophercloud.ProviderClient) (*gophercloud.ServiceClient, error) {
	return openstack.NewComputeV2(provider,
		gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic},
	)
}

func (p *novaPlugin) GlanceClient(provider *gophercloud.ProviderClient) (*gophercloud.ServiceClient, error) {
	return openstack.NewImageServiceV2(provider,
		gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic},
	)
}

//Scrape implements the limes.QuotaPlugin interface.
func (p *novaPlugin) Scrape(provider *gophercloud.ProviderClient, clusterID, domainUUID, projectUUID string) (map[string]limes.ResourceData, error) {
	client, err := p.Client(provider)
	if err != nil {
		return nil, err
	}

	var limitsData struct {
		Limits struct {
			Absolute struct {
				MaxTotalCores      int64  `json:"maxTotalCores"`
				MaxTotalInstances  int64  `json:"maxTotalInstances"`
				MaxTotalRAMSize    int64  `json:"maxTotalRAMSize"`
				TotalCoresUsed     uint64 `json:"totalCoresUsed"`
				TotalInstancesUsed uint64 `json:"totalInstancesUsed"`
				TotalRAMUsed       uint64 `json:"totalRAMUsed"`
			} `json:"absolute"`
			AbsolutePerFlavor map[string]struct {
				MaxTotalInstances  int64  `json:"maxTotalInstances"`
				TotalInstancesUsed uint64 `json:"totalInstancesUsed"`
			} `json:"absolutePerFlavor"`
		} `json:"limits"`
	}
	err = limits.Get(client, limits.GetOpts{TenantID: projectUUID}).ExtractInto(&limitsData)
	if err != nil {
		return nil, err
	}

	result := map[string]*limes.ResourceData{
		"cores": {
			Quota: limitsData.Limits.Absolute.MaxTotalCores,
			Usage: limitsData.Limits.Absolute.TotalCoresUsed,
		},
		"instances": {
			Quota: limitsData.Limits.Absolute.MaxTotalInstances,
			Usage: limitsData.Limits.Absolute.TotalInstancesUsed,
		},
		"ram": {
			Quota: limitsData.Limits.Absolute.MaxTotalRAMSize,
			Usage: limitsData.Limits.Absolute.TotalRAMUsed,
		},
	}
	if limitsData.Limits.AbsolutePerFlavor != nil {
		for flavorName, flavorLimits := range limitsData.Limits.AbsolutePerFlavor {
			result["instances_"+flavorName] = &limes.ResourceData{
				Quota: flavorLimits.MaxTotalInstances,
				Usage: flavorLimits.TotalInstancesUsed,
			}
		}
	}

	if p.scrapeInstances {
		listOpts := novaServerListOpts{
			AllTenants: true,
			TenantID:   projectUUID,
		}

		err := servers.List(client, listOpts).EachPage(func(page pagination.Page) (bool, error) {
			instances, err := servers.ExtractServers(page)
			if err != nil {
				return false, err
			}

			for _, instance := range instances {
				subResource := map[string]interface{}{
					"id":     instance.ID,
					"name":   instance.Name,
					"status": instance.Status,
				}
				flavorID := instance.Flavor["id"].(string)
				flavor, err := p.getFlavor(client, flavorID)
				if err == nil {
					subResource["flavor"] = flavor.Name
					subResource["vcpu"] = flavor.VCPUs
					subResource["ram"] = limes.ValueWithUnit{
						Value: uint64(flavor.RAM),
						Unit:  limes.UnitMebibytes,
					}
					subResource["disk"] = limes.ValueWithUnit{
						Value: uint64(flavor.Disk),
						Unit:  limes.UnitGibibytes,
					}
				} else {
					util.LogError("error while trying to retrieve data for flavor %s: %s", flavorID, err.Error())
				}

				imageID, ok := instance.Image["id"].(string)
				if ok {
					osType, err := p.getOSType(provider, imageID)
					if err == nil {
						subResource["os_type"] = osType
					} else {
						util.LogError("error while trying to find OS type for image %s: %s", imageID, err.Error())
					}
				} else {
					subResource["os_type"] = "image-missing"
				}

				resource, exists := result["instances_"+flavor.Name]
				if !exists {
					resource = result["instances"]
				}
				resource.Subresources = append(resource.Subresources, subResource)
			}
			return true, nil
		})
		if err != nil {
			return nil, err
		}
	}

	//remove references (which we needed to apply the subresources correctly)
	result2 := make(map[string]limes.ResourceData, len(result))
	for name, data := range result {
		result2[name] = *data
	}
	return result2, nil
}

//SetQuota implements the limes.QuotaPlugin interface.
func (p *novaPlugin) SetQuota(provider *gophercloud.ProviderClient, clusterID, domainUUID, projectUUID string, quotas map[string]uint64) error {
	client, err := p.Client(provider)
	if err != nil {
		return err
	}

	return quotasets.Update(client, projectUUID, novaQuotaUpdateOpts(quotas)).Err
}

//Getting and caching flavor details
//Changing a flavor is not supported from OpenStack, so no invalidating of the cache needed
//Acces to the map is not thread safe
func (p *novaPlugin) getFlavor(client *gophercloud.ServiceClient, flavorID string) (*flavors.Flavor, error) {
	if p.flavors == nil {
		p.flavors = make(map[string]*flavors.Flavor)
	}

	if flavor, ok := p.flavors[flavorID]; ok {
		return flavor, nil
	}

	flavor, err := flavors.Get(client, flavorID).Extract()
	if err == nil {
		p.flavors[flavorID] = flavor
	}
	return flavor, err
}

func (p *novaPlugin) getOSType(provider *gophercloud.ProviderClient, imageID string) (string, error) {
	if p.osTypeForImage == nil {
		p.osTypeForImage = make(map[string]string)
	}

	if osType, ok := p.osTypeForImage[imageID]; ok {
		return osType, nil
	}

	osType, err := p.findOSType(provider, imageID)
	if err == nil {
		p.osTypeForImage[imageID] = osType
	} else {
		fmt.Printf("internal error: %#v\n", err)
	}
	return osType, err
}

func (p *novaPlugin) findOSType(provider *gophercloud.ProviderClient, imageID string) (string, error) {
	client, err := p.GlanceClient(provider)
	if err != nil {
		return "", err
	}
	var result struct {
		Tags         []string `json:"tags"`
		VMwareOSType string   `json:"vmware_ostype"`
	}
	err = images.Get(client, imageID).ExtractInto(&result)
	if err != nil {
		//report a dummy value if image has been deleted...
		if _, ok := err.(gophercloud.ErrDefault404); ok {
			return "image-deleted", nil
		}
		//otherwise, try to GET image again during next scrape
		return "", err
	}

	//prefer vmware_ostype attribute since this is validated by Nova upon booting the VM
	if isValidVMwareOSType[result.VMwareOSType] {
		return result.VMwareOSType, nil
	}
	//look for a tag like "ostype:rhel7" or "ostype:windows8Server64"
	osType := ""
	for _, tag := range result.Tags {
		if strings.HasPrefix(tag, "ostype:") {
			if osType == "" {
				osType = strings.TrimPrefix(tag, "ostype:")
			} else {
				//multiple such tags -> wtf
				osType = ""
				break
			}
		}
	}

	//report "unknown" as a last resort
	if osType == "" {
		osType = "unknown"
	}
	return osType, nil
}

func makeIntPointer(value int) *int {
	return &value
}

type novaServerListOpts struct {
	AllTenants bool   `q:"all_tenants"`
	TenantID   string `q:"tenant_id"`
}

func (opts novaServerListOpts) ToServerListQuery() (string, error) {
	q, err := gophercloud.BuildQueryString(opts)
	return q.String(), err
}

type novaQuotaUpdateOpts map[string]uint64

func (opts novaQuotaUpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) {
	result := make(map[string]interface{}, len(opts))
	for key, val := range opts {
		result[key] = val
	}
	return map[string]interface{}{"quota_set": result}, nil
}
<commit_msg>add attribute "hypervisor" to instance subresources<commit_after>/*******************************************************************************
*
* Copyright 2017 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You should have received a copy of the License along with this
* program. If not, you may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*******************************************************************************/

package plugins

import (
	"fmt"
	"sort"
	"strings"

	"github.com/gophercloud/gophercloud"
	"github.com/gophercloud/gophercloud/openstack"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/limits"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/quotasets"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/flavors"
	"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
	"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
	"github.com/gophercloud/gophercloud/pagination"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/sapcc/limes/pkg/limes"
	"github.com/sapcc/limes/pkg/util"
)

//use a name that's unique to github.com/gophercloud/gophercloud/openstack/imageservice/v2/images
//to ensure that goimports does not mistakenly replace it by .../compute/v2/images
var _ images.ImageVisibility

type novaPlugin struct {
	cfg             limes.ServiceConfiguration
	scrapeInstances bool
	resources       []limes.ResourceInfo
	flavors         map[string]*flavors.Flavor
	extraSpecs      map[string]map[string]string
	osTypeForImage  map[string]string
}

var novaDefaultResources = []limes.ResourceInfo{
	{
		Name: "cores",
		Unit: limes.UnitNone,
	},
	{
		Name: "instances",
		Unit: limes.UnitNone,
	},
	{
		Name: "ram",
		Unit: limes.UnitMebibytes,
	},
}

var novaInstanceCountGauge = prometheus.NewGaugeVec(
	prometheus.GaugeOpts{
		Name: "limes_instance_counts",
		Help: "Number of Nova instances per project and hypervisor type.",
	},
	[]string{"os_cluster", "domain_id", "project_id", "hypervisor"},
)

func init() {
	limes.RegisterQuotaPlugin(func(c limes.ServiceConfiguration, scrapeSubresources map[string]bool) limes.QuotaPlugin {
		return &novaPlugin{
			cfg:             c,
			scrapeInstances: scrapeSubresources["instances"],
			resources:       novaDefaultResources,
		}
	})
	prometheus.MustRegister(novaInstanceCountGauge)
}

//Init implements the limes.QuotaPlugin interface.
func (p *novaPlugin) Init(provider *gophercloud.ProviderClient) error {
	client, err := p.Client(provider)
	if err != nil {
		return err
	}

	resources, err := listPerFlavorInstanceResources(client)
	if err != nil {
		return err
	}
	for _, resourceName := range resources {
		p.resources = append(p.resources, limes.ResourceInfo{
			Name:     resourceName,
			Category: "per_flavor",
			Unit:     limes.UnitNone,
		})
	}

	sort.Slice(p.resources, func(i, j int) bool {
		return p.resources[i].Name < p.resources[j].Name
	})
	return nil
}

//ServiceInfo implements the limes.QuotaPlugin interface.
func (p *novaPlugin) ServiceInfo() limes.ServiceInfo {
	return limes.ServiceInfo{
		Type:        "compute",
		ProductName: "nova",
		Area:        "compute",
	}
}

//Resources implements the limes.QuotaPlugin interface.
func (p *novaPlugin) Resources() []limes.ResourceInfo {
	return p.resources
}

func (p *novaPlugin) Client(provider *gophercloud.ProviderClient) (*gophercloud.ServiceClient, error) {
	return openstack.NewComputeV2(provider,
		gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic},
	)
}

func (p *novaPlugin) GlanceClient(provider *gophercloud.ProviderClient) (*gophercloud.ServiceClient, error) {
	return openstack.NewImageServiceV2(provider,
		gophercloud.EndpointOpts{Availability: gophercloud.AvailabilityPublic},
	)
}

//Scrape implements the limes.QuotaPlugin interface.
func (p *novaPlugin) Scrape(provider *gophercloud.ProviderClient, clusterID, domainUUID, projectUUID string) (map[string]limes.ResourceData, error) {
	client, err := p.Client(provider)
	if err != nil {
		return nil, err
	}

	var limitsData struct {
		Limits struct {
			Absolute struct {
				MaxTotalCores      int64  `json:"maxTotalCores"`
				MaxTotalInstances  int64  `json:"maxTotalInstances"`
				MaxTotalRAMSize    int64  `json:"maxTotalRAMSize"`
				TotalCoresUsed     uint64 `json:"totalCoresUsed"`
				TotalInstancesUsed uint64 `json:"totalInstancesUsed"`
				TotalRAMUsed       uint64 `json:"totalRAMUsed"`
			} `json:"absolute"`
			AbsolutePerFlavor map[string]struct {
				MaxTotalInstances  int64  `json:"maxTotalInstances"`
				TotalInstancesUsed uint64 `json:"totalInstancesUsed"`
			} `json:"absolutePerFlavor"`
		} `json:"limits"`
	}
	err = limits.Get(client, limits.GetOpts{TenantID: projectUUID}).ExtractInto(&limitsData)
	if err != nil {
		return nil, err
	}

	result := map[string]*limes.ResourceData{
		"cores": {
			Quota: limitsData.Limits.Absolute.MaxTotalCores,
			Usage: limitsData.Limits.Absolute.TotalCoresUsed,
		},
		"instances": {
			Quota: limitsData.Limits.Absolute.MaxTotalInstances,
			Usage: limitsData.Limits.Absolute.TotalInstancesUsed,
		},
		"ram": {
			Quota: limitsData.Limits.Absolute.MaxTotalRAMSize,
			Usage: limitsData.Limits.Absolute.TotalRAMUsed,
		},
	}
	if limitsData.Limits.AbsolutePerFlavor != nil {
		for flavorName, flavorLimits := range limitsData.Limits.AbsolutePerFlavor {
			result["instances_"+flavorName] = &limes.ResourceData{
				Quota: flavorLimits.MaxTotalInstances,
				Usage: flavorLimits.TotalInstancesUsed,
			}
		}
	}

	if p.scrapeInstances {
		listOpts := novaServerListOpts{
			AllTenants: true,
			TenantID:   projectUUID,
		}

		countsByHypervisor := map[string]uint64{
			"vmware":  0,
			"none":    0,
			"unknown": 0,
		}

		err := servers.List(client, listOpts).EachPage(func(page pagination.Page) (bool, error) {
			instances, err := servers.ExtractServers(page)
			if err != nil {
				return false, err
			}

			for _, instance := range instances {
				subResource := map[string]interface{}{
					"id":     instance.ID,
					"name":   instance.Name,
					"status": instance.Status,
				}
				flavorID := instance.Flavor["id"].(string)
				flavor, _, err := p.getFlavor(client, flavorID)
				if err == nil {
					subResource["flavor"] = flavor.Name
					subResource["vcpu"] = flavor.VCPUs
					subResource["ram"] = limes.ValueWithUnit{
						Value: uint64(flavor.RAM),
						Unit:  limes.UnitMebibytes,
					}
					subResource["disk"] = limes.ValueWithUnit{
						Value: uint64(flavor.Disk),
						Unit:  limes.UnitGibibytes,
					}
				} else {
					util.LogError("error while trying to retrieve data for flavor %s: %s", flavorID, err.Error())
				}

				hypervisorType, err := p.getHypervisorType(client, flavorID)
				if err != nil {
					util.LogError("error while trying to find hypervisor type for flavor %s: %s", flavorID, err.Error())
				}
				subResource["hypervisor"] = hypervisorType
				countsByHypervisor[hypervisorType]++

				imageID, ok := instance.Image["id"].(string)
				if ok {
					osType, err := p.getOSType(provider, imageID)
					if err == nil {
						subResource["os_type"] = osType
					} else {
						util.LogError("error while trying to find OS type for image %s: %s", imageID, err.Error())
					}
				} else {
					subResource["os_type"] = "image-missing"
				}

				resource, exists := result["instances_"+flavor.Name]
				if !exists {
					resource = result["instances"]
				}
				resource.Subresources = append(resource.Subresources, subResource)
			}
			return true, nil
		})
		if err != nil {
			return nil, err
		}

		//report Prometheus metrics
		for typeStr, count := range countsByHypervisor {
			novaInstanceCountGauge.With(prometheus.Labels{
				"os_cluster": clusterID,
				"domain_id":  domainUUID,
				"project_id": projectUUID,
				"hypervisor": typeStr,
			}).Set(float64(count))
		}
	}

	//remove references (which we needed to apply the subresources correctly)
	result2 := make(map[string]limes.ResourceData, len(result))
	for name, data := range result {
		result2[name] = *data
	}
	return result2, nil
}

//SetQuota implements the limes.QuotaPlugin interface.
func (p *novaPlugin) SetQuota(provider *gophercloud.ProviderClient, clusterID, domainUUID, projectUUID string, quotas map[string]uint64) error {
	client, err := p.Client(provider)
	if err != nil {
		return err
	}

	return quotasets.Update(client, projectUUID, novaQuotaUpdateOpts(quotas)).Err
}

//Getting and caching flavor details and extra specs
//Changing a flavor is not supported from OpenStack, so no invalidating of the cache needed
//Access to the map is not thread safe
func (p *novaPlugin) getFlavor(client *gophercloud.ServiceClient, flavorID string) (*flavors.Flavor, map[string]string, error) {
	if p.flavors == nil {
		p.flavors = make(map[string]*flavors.Flavor)
	}
	if p.extraSpecs == nil {
		p.extraSpecs = make(map[string]map[string]string)
	}

	if _, ok := p.flavors[flavorID]; !ok {
		flavor, err := flavors.Get(client, flavorID).Extract()
		if err != nil {
			return nil, nil, err
		}
		p.flavors[flavorID] = flavor
	}

	if _, ok := p.extraSpecs[flavorID]; !ok {
		specs, err := getFlavorExtras(client, flavorID)
		if err != nil {
			return nil, nil, err
		}
		p.extraSpecs[flavorID] = specs
	}

	return p.flavors[flavorID], p.extraSpecs[flavorID], nil
}

func (p *novaPlugin) getHypervisorType(client *gophercloud.ServiceClient, flavorID string) (string, error) {
	_, extraSpecs, err := p.getFlavor(client, flavorID)
	if err != nil {
		return "unknown", err
	}

	if extraSpecs["vmware:hv_enabled"] == "True" {
		return "vmware", nil
	}
	if _, ok := extraSpecs["capabilities:cpu_arch"]; ok {
		return "none", nil //looks like a bare-metal flavor
	}
	return "unknown", nil
}

func (p *novaPlugin) getOSType(provider *gophercloud.ProviderClient, imageID string) (string, error) {
	if p.osTypeForImage == nil {
		p.osTypeForImage = make(map[string]string)
	}

	if osType, ok := p.osTypeForImage[imageID]; ok {
		return osType, nil
	}

	osType, err := p.findOSType(provider, imageID)
	if err == nil {
		p.osTypeForImage[imageID] = osType
	} else {
		fmt.Printf("internal error: %#v\n", err)
	}
	return osType, err
}

func (p *novaPlugin) findOSType(provider *gophercloud.ProviderClient, imageID string) (string, error) {
	client, err := p.GlanceClient(provider)
	if err != nil {
		return "", err
	}
	var result struct {
		Tags         []string `json:"tags"`
		VMwareOSType string   `json:"vmware_ostype"`
	}
	err = images.Get(client, imageID).ExtractInto(&result)
	if err != nil {
		//report a dummy value if image has been deleted...
		if _, ok := err.(gophercloud.ErrDefault404); ok {
			return "image-deleted", nil
		}
		//otherwise, try to GET image again during next scrape
		return "", err
	}

	//prefer vmware_ostype attribute since this is validated by Nova upon booting the VM
	if isValidVMwareOSType[result.VMwareOSType] {
		return result.VMwareOSType, nil
	}
	//look for a tag like "ostype:rhel7" or "ostype:windows8Server64"
	osType := ""
	for _, tag := range result.Tags {
		if strings.HasPrefix(tag, "ostype:") {
			if osType == "" {
				osType = strings.TrimPrefix(tag, "ostype:")
			} else {
				//multiple such tags -> wtf
				osType = ""
				break
			}
		}
	}

	//report "unknown" as a last resort
	if osType == "" {
		osType = "unknown"
	}
	return osType, nil
}

func makeIntPointer(value int) *int {
	return &value
}

type novaServerListOpts struct {
	AllTenants bool   `q:"all_tenants"`
	TenantID   string `q:"tenant_id"`
}

func (opts novaServerListOpts) ToServerListQuery() (string, error) {
	q, err := gophercloud.BuildQueryString(opts)
	return q.String(), err
}

type novaQuotaUpdateOpts map[string]uint64

func (opts novaQuotaUpdateOpts) ToComputeQuotaUpdateMap() (map[string]interface{}, error) {
	result := make(map[string]interface{}, len(opts))
	for key, val := range opts {
		result[key] = val
	}
	return map[string]interface{}{"quota_set": result}, nil
}
<|endoftext|>


<commit_before>package runtime

import (
	"os"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)

type Signal <-chan struct{}

/* TODO(jdef) not very useful without execution conditional on errors
// upon receiving signal sig invoke function f and immediately return a signal
// that indicates f's completion.
func (sig Signal) Then(f func()) Signal {
	if sig == nil {
		return nil
	}
	ch := make(chan struct{})
	On(sig, func() {
		defer close(ch)
		f()
	})
	return Signal(ch)
}
*/

// execute a callback function after the specified signal chan closes.
// immediately returns a signal that indicates f's completion.
func On(sig <-chan struct{}, f func()) Signal {
	if sig == nil {
		return nil
	}
	return Go(func() {
		<-sig
		f()
	})
}

func OnOSSignal(sig <-chan os.Signal, f func(os.Signal)) Signal {
	if sig == nil {
		return nil
	}
	return Go(func() {
		if s, ok := <-sig; ok {
			f(s)
		}
	})
}

// spawn a goroutine to execute a func, immediately returns a chan that closes
// upon completion of the func.
func Go(f func()) Signal {
	ch := make(chan struct{})
	go func() {
		defer close(ch)
		defer util.HandleCrash()
		f()
	}()
	return Signal(ch)
}
<commit_msg>prevent invocation of nil funcs<commit_after>package runtime

import (
	"os"

	"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
)

type Signal <-chan struct{}

/* TODO(jdef) not very useful without execution conditional on errors
// upon receiving signal sig invoke function f and immediately return a signal
// that indicates f's completion.
func (sig Signal) Then(f func()) Signal {
	if sig == nil {
		return nil
	}
	ch := make(chan struct{})
	On(sig, func() {
		defer close(ch)
		f()
	})
	return Signal(ch)
}
*/

// execute a callback function after the specified signal chan closes.
// immediately returns a signal that indicates f's completion.
func On(sig <-chan struct{}, f func()) Signal {
	if sig == nil {
		return nil
	}
	return Go(func() {
		<-sig
		if f != nil {
			f()
		}
	})
}

func OnOSSignal(sig <-chan os.Signal, f func(os.Signal)) Signal {
	if sig == nil {
		return nil
	}
	return Go(func() {
		if s, ok := <-sig; ok && f != nil {
			f(s)
		}
	})
}

// spawn a goroutine to execute a func, immediately returns a chan that closes
// upon completion of the func. returns a nil signal chan if the given func is nil.
func Go(f func()) Signal {
	ch := make(chan struct{})
	go func() {
		defer close(ch)
		defer util.HandleCrash()
		if f != nil {
			f()
		}
	}()
	return Signal(ch)
}
<|endoftext|>


<commit_before>package slinga

import (
	"fmt"
	log "github.com/Sirupsen/logrus"
)

import (
	"k8s.io/kubernetes/pkg/api"
	k8slabels "k8s.io/kubernetes/pkg/labels"
)

// ProcessIstioIngress processes global rules and applies Istio routing rules for ingresses
func (usage *ServiceUsageState) ProcessIstioIngress(noop bool) {
	if len(usage.getResolvedUsage().ComponentProcessingOrder) == 0 || noop {
		return
	}

	fmt.Println("[Routes]")

	progress := NewProgress()
	progressBar := AddProgressBar(progress, len(usage.getResolvedUsage().ComponentProcessingOrder))

	desiredBlockedServices := make([]string, 0)

	// Process in the right order
	for _, key := range usage.getResolvedUsage().ComponentProcessingOrder {
		services, err := processComponent(key, usage)
		if err != nil {
			debug.WithFields(log.Fields{
				"key":   key,
				"error": err,
			}).Fatal("Unable to process Istio Ingress for component")
		}
		desiredBlockedServices = append(desiredBlockedServices, services...)
		progressBar.Incr()
	}

	progress.Stop()

	if len(desiredBlockedServices) > 0 {
		fmt.Println("[Blocked Services]", desiredBlockedServices)
	}

	// todo(slukjanov): add actual istio route rules creation/deletion here
}

func processComponent(key string, usage *ServiceUsageState) ([]string, error) {
	serviceName, _, _, componentName := ParseServiceUsageKey(key)
	component := usage.Policy.Services[serviceName].getComponentsMap()[componentName]

	labels := usage.ResolvedUsage.ComponentInstanceMap[key].CalculatedLabels

	// todo(slukjanov): temp hack - expecting that cluster is always passed through the label "cluster"
	var cluster *Cluster
	if clusterLabel, ok := labels.Labels["cluster"]; ok {
		if cluster, ok = usage.Policy.Clusters[clusterLabel]; !ok {
			debug.WithFields(log.Fields{
				"component": key,
				"labels":    labels.Labels,
			}).Fatal("Can't find cluster for component (based on label 'cluster')")
		}
	}

	// get all users who're using service
	userIds := usage.ResolvedUsage.ComponentInstanceMap[key].UserIds
	users := make([]*User, 0)
	for _, userID := range userIds {
		// todo check if user doesn't exists
		users = append(users, usage.users.Users[userID])
	}

	if !usage.Policy.Rules.allowsIngressAccess(labels, users, cluster) && component != nil && component.Code != nil {
		codeExecutor, err := component.Code.GetCodeExecutor(key, component.Code.Metadata, usage.getResolvedUsage().ComponentInstanceMap[key].CalculatedCodeParams, usage.Policy.Clusters)
		if err != nil {
			return nil, err
		}

		if helmCodeExecutor, ok := codeExecutor.(HelmCodeExecutor); ok {
			services, err := helmCodeExecutor.httpServices()
			if err != nil {
				return nil, err
			}

			for _, service := range services {
				content := "type: route-rule\n"
				content += "name: block-" + service + "\n"
				content += "spec:\n"
				content += "  destination: " + service + "." + cluster.Metadata.Namespace + ".svc.cluster.local\n"
				content += "  httpReqTimeout:\n"
				content += "    simpleTimeout:\n"
				content += "      timeout: 1ms\n"

				ruleFile := writeTempFile("istio-rule", content)
				fmt.Println("Istio rule file:", ruleFile.Name())
				//TODO: slukjanov: defer os.Remove(tmpFile.Name())

				content = "set -ex\n"
				content += "kubectl config use-context " + cluster.Name + "\n"
				// todo(slukjanov): find istio pilot service automatically
				content += "istioctl --configAPIService istio-prod-production-istio-istio-pilot:8081 --namespace " + cluster.Metadata.Namespace + " "
				content += "create -f " + ruleFile.Name() + "\n"

				cmdFile := writeTempFile("istioctl", content)
				fmt.Println("Istio cmd file:", cmdFile.Name())
			}

			return services, nil
		}
	}

	return nil, nil
}

// httpServices returns list of services for the current chart
func (exec HelmCodeExecutor) httpServices() ([]string, error) {
	_, clientset, err := exec.newKubeClient()
	if err != nil {
		return nil, err
	}

	coreClient := clientset.Core()

	releaseName := releaseName(exec.Key)
	chartName := exec.Metadata["chartName"]

	selector := k8slabels.Set{"release": releaseName, "chart": chartName}.AsSelector()
	options := api.ListOptions{LabelSelector: selector}

	// Check all corresponding services
	services, err := coreClient.Services(exec.Cluster.Metadata.Namespace).List(options)
	if err != nil {
		return nil, err
	}

	// Check all corresponding Istio ingresses
	ingresses, err := clientset.Extensions().Ingresses(exec.Cluster.Metadata.Namespace).List(options)
	if err != nil {
		return nil, err
	}

	if len(ingresses.Items) > 0 {
		result := make([]string, 0)
		for _, service := range services.Items {
			result = append(result, service.Name)
		}

		return result, nil
	}

	return nil, nil
}
<commit_msg>Impl full support for istio route rules to block services<commit_after>package slinga

import (
	"fmt"
	log "github.com/Sirupsen/logrus"
	"k8s.io/kubernetes/pkg/api"
	k8slabels "k8s.io/kubernetes/pkg/labels"
	"strings"
)

type IstioRouteRule struct {
	Service string
	Cluster *Cluster
}

// ProcessIstioIngress processes global rules and applies Istio routing rules for ingresses
func (usage *ServiceUsageState) ProcessIstioIngress(noop bool) {
	if len(usage.getResolvedUsage().ComponentProcessingOrder) == 0 || noop {
		return
	}

	fmt.Println("[Route Rules (Istio)]")

	progress := NewProgress()
	progressBar := AddProgressBar(progress, len(usage.getResolvedUsage().ComponentProcessingOrder) + len(usage.Policy.Clusters))

	existingRules := make([]*IstioRouteRule, 0)

	for _, cluster := range usage.Policy.Clusters {
		existingRules = append(existingRules, cluster.getIstioRouteRules()...)

		progressBar.Incr()
	}

	desiredRules := make([]*IstioRouteRule, 0)

	// Process in the right order
	for _, key := range usage.getResolvedUsage().ComponentProcessingOrder {
		rules, err := processComponent(key, usage)
		if err != nil {
			debug.WithFields(log.Fields{
				"key":   key,
				"error": err,
			}).Fatal("Unable to process Istio Ingress for component")
		}
		desiredRules = append(desiredRules, rules...)
		progressBar.Incr()
	}

	deleteRules := make([]*IstioRouteRule, 0)
	createRules := make([]*IstioRouteRule, 0)

	for _, existingRule := range existingRules {
		found := false
		for _, desiredRule := range desiredRules {
			if existingRule.Service == desiredRule.Service && existingRule.Cluster.Name == desiredRule.Cluster.Name {
				found = true
			}
		}
		if !found {
			deleteRules = append(deleteRules, existingRule)
		}
	}

	for _, desiredRule := range desiredRules {
		found := false
		for _, existingRule := range existingRules {
			if desiredRule.Service == existingRule.Service && desiredRule.Cluster.Name == existingRule.Cluster.Name {
				found = true
			}
		}
		if !found {
			createRules = append(createRules, desiredRule)
		}
	}

	tbdLen := len(createRules) + len(deleteRules)

	if tbdLen > 0 {
		progressBar = AddProgressBar(progress, tbdLen)

		for _, rule := range createRules {
			rule.create()
			progressBar.Incr()
		}

		for _, rule := range deleteRules {
			rule.delete()
			progressBar.Incr()
		}
	}

	progress.Stop()

	if tbdLen > 0 {
		for _, rule := range deleteRules {
			fmt.Println("  [-] " + rule.Service)
		}
		for _, rule := range createRules {
			fmt.Println("  [+] " + rule.Service)
		}
	} else {
		fmt.Println("  [*] No changes")
	}

	// todo(slukjanov): add actual istio route rules creation/deletion here
}

func processComponent(key string, usage *ServiceUsageState) ([]*IstioRouteRule, error) {
	serviceName, _, _, componentName := ParseServiceUsageKey(key)
	component := usage.Policy.Services[serviceName].getComponentsMap()[componentName]

	labels := usage.ResolvedUsage.ComponentInstanceMap[key].CalculatedLabels

	// todo(slukjanov): temp hack - expecting that cluster is always passed through the label "cluster"
	var cluster *Cluster
	if clusterLabel, ok := labels.Labels["cluster"]; ok {
		if cluster, ok = usage.Policy.Clusters[clusterLabel]; !ok {
			debug.WithFields(log.Fields{
				"component": key,
				"labels":    labels.Labels,
			}).Fatal("Can't find cluster for component (based on label 'cluster')")
		}
	}

	// get all users who're using service
	userIds := usage.ResolvedUsage.ComponentInstanceMap[key].UserIds
	users := make([]*User, 0)
	for _, userID := range userIds {
		// todo check if user doesn't exists
		users = append(users, usage.users.Users[userID])
	}

	if !usage.Policy.Rules.allowsIngressAccess(labels, users, cluster) && component != nil && component.Code != nil {
		codeExecutor, err := component.Code.GetCodeExecutor(key, component.Code.Metadata, usage.getResolvedUsage().ComponentInstanceMap[key].CalculatedCodeParams, usage.Policy.Clusters)
		if err != nil {
			return nil, err
		}

		if helmCodeExecutor, ok := codeExecutor.(HelmCodeExecutor); ok {
			services, err := helmCodeExecutor.httpServices()
			if err != nil {
				return nil, err
			}

			rules := make([]*IstioRouteRule, 0)

			for _, service := range services {
				rules = append(rules, &IstioRouteRule{service, cluster})
			}

			return rules, nil
		}
	}

	return nil, nil
}

// httpServices returns list of services for the current chart
func (exec HelmCodeExecutor) httpServices() ([]string, error) {
	_, clientset, err := exec.Cluster.newKubeClient()
	if err != nil {
		return nil, err
	}

	coreClient := clientset.Core()

	releaseName := releaseName(exec.Key)
	chartName := exec.Metadata["chartName"]

	selector := k8slabels.Set{"release": releaseName, "chart": chartName}.AsSelector()
	options := api.ListOptions{LabelSelector: selector}

	// Check all corresponding services
	services, err := coreClient.Services(exec.Cluster.Metadata.Namespace).List(options)
	if err != nil {
		return nil, err
	}

	// Check all corresponding Istio ingresses
	ingresses, err := clientset.Extensions().Ingresses(exec.Cluster.Metadata.Namespace).List(options)
	if err != nil {
		return nil, err
	}

	if len(ingresses.Items) > 0 {
		result := make([]string, 0)
		for _, service := range services.Items {
			result = append(result, service.Name)
		}

		return result, nil
	}

	return nil, nil
}

func (cluster *Cluster) getIstioRouteRules() []*IstioRouteRule {
	cmd := "get route-rules"
	rulesStr, err := cluster.runIstioCmd(cmd)
	if err != nil {
		debug.WithFields(log.Fields{
			"cluster": cluster.Name,
			"cmd": cmd,
			"error": err,
		}).Fatal("Failed to get route-rules by running bash cmd")
	}

	rules := make([]*IstioRouteRule, 0)

	for _, ruleName := range strings.Split(rulesStr, "\n") {
		if ruleName == "" {
			continue
		}
		rules = append(rules, &IstioRouteRule{ruleName, cluster})
	}

	return rules
}

func (rule *IstioRouteRule) create() {
	content := "type: route-rule\n"
	content += "name: " + rule.Service + "\n"
	content += "spec:\n"
	content += "  destination: " + rule.Service + "." + rule.Cluster.Metadata.Namespace + ".svc.cluster.local\n"
	content += "  httpReqTimeout:\n"
	content += "    simpleTimeout:\n"
	content += "      timeout: 1ms\n"

	ruleFile := writeTempFile("istio-rule", content)

	out, err := rule.Cluster.runIstioCmd("create -f " + ruleFile.Name())
	if err != nil {
		debug.WithFields(log.Fields{
			"cluster": rule.Cluster.Name,
			"content": content,
			"out": out,
			"error": err,
		}).Fatal("Failed to create istio rule by running bash script")
	}
}

func (rule *IstioRouteRule) delete() {
	out, err := rule.Cluster.runIstioCmd("delete route-rule " + rule.Service)
	if err != nil {
		debug.WithFields(log.Fields{
			"cluster": rule.Cluster.Name,
			"out": out,
			"error": err,
		}).Fatal("Failed to delete istio rule by running bash script")
	}
}

func (cluster *Cluster) runIstioCmd(cmd string) (string, error) {
	istioSvc := cluster.Metadata.istioSvc
	if istioSvc == "" {
		_, clientset, err := cluster.newKubeClient()
		if err != nil {
			return "", err
		}

		coreClient := clientset.Core()

		selector := k8slabels.Set{"app": "istio"}.AsSelector()
		options := api.ListOptions{LabelSelector: selector}

		services, err := coreClient.Services(cluster.Metadata.Namespace).List(options)
		if err != nil {
			return "", err
		}

		for _, service := range services.Items {
			if strings.Contains(service.Name, "pilot") {
				istioSvc = service.Name

				for _, port := range service.Spec.Ports {
					if port.Name == "http-apiserver" {
						istioSvc = fmt.Sprintf("%s:%d", istioSvc, port.Port)
						break
					}
				}

				cluster.Metadata.istioSvc = istioSvc
				break
			}
		}
	}

	content := "set -e\n"
	content += "kubectl config use-context " + cluster.Name + " 1>/dev/null\n"
	content += "istioctl --configAPIService " + cluster.Metadata.istioSvc + " --namespace " + cluster.Metadata.Namespace + " "
	content += cmd + "\n"

	cmdFile := writeTempFile("istioctl-cmd", content)

	out, err := runCmd("bash", cmdFile.Name())
	if err != nil {
		return "", err
	}

	return out, nil
}
<|endoftext|>


<commit_before>package telegram

import (
	"context"
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"

	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/hako/durafmt"
	"github.com/metalmatze/alertmanager-bot/pkg/alertmanager"
	"github.com/oklog/run"
	"github.com/prometheus/alertmanager/notify"
	"github.com/prometheus/alertmanager/template"
	"github.com/prometheus/alertmanager/types"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tucnak/telebot"
)

const (
	commandStart = "/start"
	commandStop  = "/stop"
	commandHelp  = "/help"
	commandChats = "/chats"

	commandStatus     = "/status"
	commandAlerts     = "/alerts"
	commandSilences   = "/silences"
	commandSilenceAdd = "/silence_add"
	commandSilence    = "/silence"
	commandSilenceDel = "/silence_del"

	responseStart = "Hey, %s! I will now keep you up to date!\n" + commandHelp
	responseStop  = "Alright, %s! I won't talk to you again.\n" + commandHelp
	responseHelp  = `
I'm a Prometheus AlertManager Bot for Telegram. I will notify you about alerts.
You can also ask me about my ` + commandStatus + `, ` + commandAlerts + ` & ` + commandSilences + `

Available commands:
` + commandStart + ` - Subscribe for alerts.
` + commandStop + ` - Unsubscribe for alerts.
` + commandStatus + ` - Print the current status.
` + commandAlerts + ` - List all alerts.
` + commandSilences + ` - List all silences.
`
)

// BotChatStore is all the Bot needs to store and read
type BotChatStore interface {
	List() ([]telebot.Chat, error)
	Add(telebot.Chat) error
	Remove(telebot.Chat) error
}

// Bot runs the alertmanager telegram
type Bot struct {
	addr         string
	admins       []int // must be kept sorted
	alertmanager *url.URL
	templates    *template.Template
	chats        BotChatStore
	logger       log.Logger
	revision     string
	startTime    time.Time

	telegram *telebot.Bot

	commandsCounter *prometheus.CounterVec
	webhooksCounter prometheus.Counter
}

// BotOption passed to NewBot to change the default instance
type BotOption func(b *Bot)

// NewBot creates a Bot with the UserStore and telegram telegram
func NewBot(chats BotChatStore, token string, admin int, opts ...BotOption) (*Bot, error) {
	bot, err := telebot.NewBot(token)
	if err != nil {
		return nil, err
	}

	commandsCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: "alertmanagerbot",
		Name:      "commands_total",
		Help:      "Number of commands received by command name",
	}, []string{"command"})
	if err := prometheus.Register(commandsCounter); err != nil {
		return nil, err
	}

	b := &Bot{
		logger:          log.NewNopLogger(),
		telegram:        bot,
		chats:           chats,
		addr:            "127.0.0.1:8080",
		admins:          []int{admin},
		alertmanager:    &url.URL{Host: "localhost:9093"},
		commandsCounter: commandsCounter,
		// TODO: initialize templates with default?
	}

	for _, opt := range opts {
		opt(b)
	}

	return b, nil
}

// WithLogger sets the logger for the Bot as an option
func WithLogger(l log.Logger) BotOption {
	return func(b *Bot) {
		b.logger = l
	}
}

// WithAddr sets the internal listening addr of the bot's web server receiving webhooks
func WithAddr(addr string) BotOption {
	return func(b *Bot) {
		b.addr = addr
	}
}

// WithAlertmanager sets the connection url for the Alertmanager
func WithAlertmanager(u *url.URL) BotOption {
	return func(b *Bot) {
		b.alertmanager = u
	}
}

// WithTemplates uses Alertmanager template to render messages for Telegram
func WithTemplates(t *template.Template) BotOption {
	return func(b *Bot) {
		b.templates = t
	}
}

// WithRevision is setting the Bot's revision for status commands
func WithRevision(r string) BotOption {
	return func(b *Bot) {
		b.revision = r
	}
}

// WithStartTime is setting the Bot's start time for status commands
func WithStartTime(st time.Time) BotOption {
	return func(b *Bot) {
		b.startTime = st
	}
}

// WithExtraAdmins allows the specified additional user IDs to issue admin
// commands to the bot.
func WithExtraAdmins(ids ...int) BotOption {
	return func(b *Bot) {
		b.admins = append(b.admins, ids...)
		sort.Ints(b.admins)
	}
}

// SendAdminMessage to the admin's ID with a message
func (b *Bot) SendAdminMessage(adminID int, message string) {
	b.telegram.SendMessage(telebot.User{ID: adminID}, message, nil)
}

// isAdminID returns whether id is one of the configured admin IDs.
func (b *Bot) isAdminID(id int) bool {
	i := sort.SearchInts(b.admins, id)
	return i < len(b.admins) && b.admins[i] == id
}

// Run the telegram and listen to messages send to the telegram
func (b *Bot) Run(ctx context.Context, webhooks <-chan notify.WebhookMessage) error {
	commandSuffix := fmt.Sprintf("@%s", b.telegram.Identity.Username)

	commands := map[string]func(message telebot.Message){
		commandStart:    b.handleStart,
		commandStop:     b.handleStop,
		commandHelp:     b.handleHelp,
		commandChats:    b.handleChats,
		commandStatus:   b.handleStatus,
		commandAlerts:   b.handleAlerts,
		commandSilences: b.handleSilences,
	}

	// init counters with 0
	for command := range commands {
		b.commandsCounter.WithLabelValues(command).Add(0)
	}

	process := func(message telebot.Message) error {
		if message.IsService() {
			return nil
		}

		if !b.isAdminID(message.Sender.ID) {
			b.commandsCounter.WithLabelValues("dropped").Inc()
			return fmt.Errorf("dropped message from forbidden sender")
		}

		if err := b.telegram.SendChatAction(message.Chat, telebot.Typing); err != nil {
			return err
		}

		// Remove the command suffix from the text, /help@BotName => /help
		text := strings.Replace(message.Text, commandSuffix, "", -1)
		// Only take the first part into account, /help foo => /help
		text = strings.Split(text, " ")[0]

		level.Debug(b.logger).Log("msg", "message received", "text", text)

		// Get the corresponding handler from the map by the commands text
		handler, ok := commands[text]

		if !ok {
			b.commandsCounter.WithLabelValues("incomprehensible").Inc()
			b.telegram.SendMessage(
				message.Chat,
				"Sorry, I don't understand...",
				nil,
			)
			return nil
		}

		b.commandsCounter.WithLabelValues(text).Inc()
		handler(message)

		return nil
	}

	messages := make(chan telebot.Message, 100)
	b.telegram.Listen(messages, time.Second)

	var gr run.Group
	{
		gr.Add(func() error {
			return b.sendWebhook(ctx, webhooks)
		}, func(err error) {
		})
	}
	{
		gr.Add(func() error {
			for {
				select {
				case <-ctx.Done():
					return nil
				case message := <-messages:
					if err := process(message); err != nil {
						level.Info(b.logger).Log(
							"msg", "failed to process message",
							"err", err,
							"sender_id", message.Sender.ID,
							"sender_username", message.Sender.Username,
						)
					}
				}
			}
		}, func(err error) {
		})
	}

	return gr.Run()
}

// sendWebhook sends messages received via webhook to all subscribed chats
func (b *Bot) sendWebhook(ctx context.Context, webhooks <-chan notify.WebhookMessage) error {
	for {
		select {
		case <-ctx.Done():
			return nil
		case w := <-webhooks:
			chats, err := b.chats.List()
			if err != nil {
				level.Error(b.logger).Log("msg", "failed to get chat list from store", "err", err)
				continue
			}

			data := &template.Data{
				Receiver:          w.Receiver,
				Status:            w.Status,
				Alerts:            w.Alerts,
				GroupLabels:       w.GroupLabels,
				CommonLabels:      w.CommonLabels,
				CommonAnnotations: w.CommonAnnotations,
				ExternalURL:       w.ExternalURL,
			}

			out, err := b.templates.ExecuteHTMLString(`{{ template "telegram.default" . }}`, data)
			if err != nil {
				level.Warn(b.logger).Log("msg", "failed to template alerts", "err", err)
				continue
			}

			for _, chat := range chats {
				b.telegram.SendMessage(chat, out, &telebot.SendOptions{ParseMode: telebot.ModeHTML})
			}
		}
	}

	return nil
}

func (b *Bot) handleStart(message telebot.Message) {
	if err := b.chats.Add(message.Chat); err != nil {
		level.Warn(b.logger).Log("msg", "failed to add chat to chat store", "err", err)
		b.telegram.SendMessage(message.Chat, "I can't add this chat to the subscribers list.", nil)
		return
	}

	b.telegram.SendMessage(message.Chat, fmt.Sprintf(responseStart, message.Sender.FirstName), nil)
	level.Info(b.logger).Log(
		"user subscribed",
		"username", message.Sender.Username,
		"user_id", message.Sender.ID,
	)
}

func (b *Bot) handleStop(message telebot.Message) {
	if err := b.chats.Remove(message.Chat); err != nil {
		level.Warn(b.logger).Log("msg", "failed to remove chat from chat store", "err", err)
		b.telegram.SendMessage(message.Chat, "I can't remove this chat from the subscribers list.", nil)
		return
	}

	b.telegram.SendMessage(message.Chat, fmt.Sprintf(responseStop, message.Sender.FirstName), nil)
	level.Info(b.logger).Log(
		"user unsubscribed",
		"username", message.Sender.Username,
		"user_id", message.Sender.ID,
	)
}

func (b *Bot) handleHelp(message telebot.Message) {
	b.telegram.SendMessage(message.Chat, responseHelp, nil)
}

func (b *Bot) handleChats(message telebot.Message) {
	chats, err := b.chats.List()
	if err != nil {
		level.Warn(b.logger).Log("msg", "failed to list chats from chat store", "err", err)
		b.telegram.SendMessage(message.Chat, "I can't list the subscribed chats.", nil)
		return
	}

	list := ""
	for _, chat := range chats {
		if chat.IsGroupChat() {
			list = list + fmt.Sprintf("@%s\n", chat.Title)
		} else {
			list = list + fmt.Sprintf("@%s\n", chat.Username)
		}
	}

	b.telegram.SendMessage(message.Chat, "Currently these chat have subscribed:\n"+list, nil)
}

func (b *Bot) handleStatus(message telebot.Message) {
	s, err := alertmanager.Status(b.logger, b.alertmanager.String())
	if err != nil {
		level.Warn(b.logger).Log("msg", "failed to get status", "err", err)
		b.telegram.SendMessage(message.Chat, fmt.Sprintf("failed to get status... %v", err), nil)
		return
	}

	uptime := durafmt.Parse(time.Since(s.Data.Uptime))
	uptimeBot := durafmt.Parse(time.Since(b.startTime))

	b.telegram.SendMessage(
		message.Chat,
		fmt.Sprintf(
			"*AlertManager*\nVersion: %s\nUptime: %s\n*AlertManager Bot*\nVersion: %s\nUptime: %s",
			s.Data.VersionInfo.Version,
			uptime,
			b.revision,
			uptimeBot,
		),
		&telebot.SendOptions{ParseMode: telebot.ModeMarkdown},
	)
}

func (b *Bot) handleAlerts(message telebot.Message) {
	alerts, err := alertmanager.ListAlerts(b.logger, b.alertmanager.String())
	if err != nil {
		b.telegram.SendMessage(message.Chat, fmt.Sprintf("failed to list alerts... %v", err), nil)
		return
	}

	if len(alerts) == 0 {
		b.telegram.SendMessage(message.Chat, "No alerts right now! 🎉", nil)
		return
	}

	out, err := b.tmplAlerts(alerts...)
	if err != nil {
		return
	}

	err = b.telegram.SendMessage(message.Chat, out, &telebot.SendOptions{
		ParseMode: telebot.ModeHTML,
	})
	if err != nil {
		level.Warn(b.logger).Log("msg", "failed to send message", "err", err)
	}
}

func (b *Bot) handleSilences(message telebot.Message) {
	silences, err := alertmanager.ListSilences(b.logger, b.alertmanager.String())
	if err != nil {
		b.telegram.SendMessage(message.Chat, fmt.Sprintf("failed to list silences... %v", err), nil)
		return
	}

	if len(silences) == 0 {
		b.telegram.SendMessage(message.Chat, "No silences right now.", nil)
		return
	}

	var out string
	for _, silence := range silences {
		out = out + alertmanager.SilenceMessage(silence) + "\n"
	}

	b.telegram.SendMessage(message.Chat, out, &telebot.SendOptions{ParseMode: telebot.ModeMarkdown})
}

func (b *Bot) tmplAlerts(alerts ...*types.Alert) (string, error) {
	data := b.templates.Data("default", nil, alerts...)

	out, err := b.templates.ExecuteHTMLString(`{{ template "telegram.default" . }}`, data)
	if err != nil {
		return "", err
	}

	return out, nil
}
<commit_msg>Remove unreachable return in telegram/bot.go<commit_after>package telegram

import (
	"context"
	"fmt"
	"net/url"
	"sort"
	"strings"
	"time"

	"github.com/go-kit/kit/log"
	"github.com/go-kit/kit/log/level"
	"github.com/hako/durafmt"
	"github.com/metalmatze/alertmanager-bot/pkg/alertmanager"
	"github.com/oklog/run"
	"github.com/prometheus/alertmanager/notify"
	"github.com/prometheus/alertmanager/template"
	"github.com/prometheus/alertmanager/types"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/tucnak/telebot"
)

const (
	commandStart = "/start"
	commandStop  = "/stop"
	commandHelp  = "/help"
	commandChats = "/chats"

	commandStatus     = "/status"
	commandAlerts     = "/alerts"
	commandSilences   = "/silences"
	commandSilenceAdd = "/silence_add"
	commandSilence    = "/silence"
	commandSilenceDel = "/silence_del"

	responseStart = "Hey, %s! I will now keep you up to date!\n" + commandHelp
	responseStop  = "Alright, %s! I won't talk to you again.\n" + commandHelp
	responseHelp  = `
I'm a Prometheus AlertManager Bot for Telegram. I will notify you about alerts.
You can also ask me about my ` + commandStatus + `, ` + commandAlerts + ` & ` + commandSilences + `

Available commands:
` + commandStart + ` - Subscribe for alerts.
` + commandStop + ` - Unsubscribe for alerts.
` + commandStatus + ` - Print the current status.
` + commandAlerts + ` - List all alerts.
` + commandSilences + ` - List all silences.
`
)

// BotChatStore is all the Bot needs to store and read
type BotChatStore interface {
	List() ([]telebot.Chat, error)
	Add(telebot.Chat) error
	Remove(telebot.Chat) error
}

// Bot runs the alertmanager telegram
type Bot struct {
	addr         string
	admins       []int // must be kept sorted
	alertmanager *url.URL
	templates    *template.Template
	chats        BotChatStore
	logger       log.Logger
	revision     string
	startTime    time.Time

	telegram *telebot.Bot

	commandsCounter *prometheus.CounterVec
	webhooksCounter prometheus.Counter
}

// BotOption passed to NewBot to change the default instance
type BotOption func(b *Bot)

// NewBot creates a Bot with the UserStore and telegram telegram
func NewBot(chats BotChatStore, token string, admin int, opts ...BotOption) (*Bot, error) {
	bot, err := telebot.NewBot(token)
	if err != nil {
		return nil, err
	}

	commandsCounter := prometheus.NewCounterVec(prometheus.CounterOpts{
		Namespace: "alertmanagerbot",
		Name:      "commands_total",
		Help:      "Number of commands received by command name",
	}, []string{"command"})
	if err := prometheus.Register(commandsCounter); err != nil {
		return nil, err
	}

	b := &Bot{
		logger:          log.NewNopLogger(),
		telegram:        bot,
		chats:           chats,
		addr:            "127.0.0.1:8080",
		admins:          []int{admin},
		alertmanager:    &url.URL{Host: "localhost:9093"},
		commandsCounter: commandsCounter,
		// TODO: initialize templates with default?
	}

	for _, opt := range opts {
		opt(b)
	}

	return b, nil
}

// WithLogger sets the logger for the Bot as an option
func WithLogger(l log.Logger) BotOption {
	return func(b *Bot) {
		b.logger = l
	}
}

// WithAddr sets the internal listening addr of the bot's web server receiving webhooks
func WithAddr(addr string) BotOption {
	return func(b *Bot) {
		b.addr = addr
	}
}

// WithAlertmanager sets the connection url for the Alertmanager
func WithAlertmanager(u *url.URL) BotOption {
	return func(b *Bot) {
		b.alertmanager = u
	}
}

// WithTemplates uses Alertmanager template to render messages for Telegram
func WithTemplates(t *template.Template) BotOption {
	return func(b *Bot) {
		b.templates = t
	}
}

// WithRevision is setting the Bot's revision for status commands
func WithRevision(r string) BotOption {
	return func(b *Bot) {
		b.revision = r
	}
}

// WithStartTime is setting the Bot's start time for status commands
func WithStartTime(st time.Time) BotOption {
	return func(b *Bot) {
		b.startTime = st
	}
}

// WithExtraAdmins allows the specified additional user IDs to issue admin
// commands to the bot.
func WithExtraAdmins(ids ...int) BotOption {
	return func(b *Bot) {
		b.admins = append(b.admins, ids...)
		sort.Ints(b.admins)
	}
}

// SendAdminMessage to the admin's ID with a message
func (b *Bot) SendAdminMessage(adminID int, message string) {
	b.telegram.SendMessage(telebot.User{ID: adminID}, message, nil)
}

// isAdminID returns whether id is one of the configured admin IDs.
func (b *Bot) isAdminID(id int) bool {
	i := sort.SearchInts(b.admins, id)
	return i < len(b.admins) && b.admins[i] == id
}

// Run the telegram and listen to messages send to the telegram
func (b *Bot) Run(ctx context.Context, webhooks <-chan notify.WebhookMessage) error {
	commandSuffix := fmt.Sprintf("@%s", b.telegram.Identity.Username)

	commands := map[string]func(message telebot.Message){
		commandStart:    b.handleStart,
		commandStop:     b.handleStop,
		commandHelp:     b.handleHelp,
		commandChats:    b.handleChats,
		commandStatus:   b.handleStatus,
		commandAlerts:   b.handleAlerts,
		commandSilences: b.handleSilences,
	}

	// init counters with 0
	for command := range commands {
		b.commandsCounter.WithLabelValues(command).Add(0)
	}

	process := func(message telebot.Message) error {
		if message.IsService() {
			return nil
		}

		if !b.isAdminID(message.Sender.ID) {
			b.commandsCounter.WithLabelValues("dropped").Inc()
			return fmt.Errorf("dropped message from forbidden sender")
		}

		if err := b.telegram.SendChatAction(message.Chat, telebot.Typing); err != nil {
			return err
		}

		// Remove the command suffix from the text, /help@BotName => /help
		text := strings.Replace(message.Text, commandSuffix, "", -1)
		// Only take the first part into account, /help foo => /help
		text = strings.Split(text, " ")[0]

		level.Debug(b.logger).Log("msg", "message received", "text", text)

		// Get the corresponding handler from the map by the commands text
		handler, ok := commands[text]

		if !ok {
			b.commandsCounter.WithLabelValues("incomprehensible").Inc()
			b.telegram.SendMessage(
				message.Chat,
				"Sorry, I don't understand...",
				nil,
			)
			return nil
		}

		b.commandsCounter.WithLabelValues(text).Inc()
		handler(message)

		return nil
	}

	messages := make(chan telebot.Message, 100)
	b.telegram.Listen(messages, time.Second)

	var gr run.Group
	{
		gr.Add(func() error {
			return b.sendWebhook(ctx, webhooks)
		}, func(err error) {
		})
	}
	{
		gr.Add(func() error {
			for {
				select {
				case <-ctx.Done():
					return nil
				case message := <-messages:
					if err := process(message); err != nil {
						level.Info(b.logger).Log(
							"msg", "failed to process message",
							"err", err,
							"sender_id", message.Sender.ID,
							"sender_username", message.Sender.Username,
						)
					}
				}
			}
		}, func(err error) {
		})
	}

	return gr.Run()
}

// sendWebhook sends messages received via webhook to all subscribed chats
func (b *Bot) sendWebhook(ctx context.Context, webhooks <-chan notify.WebhookMessage) error {
	for {
		select {
		case <-ctx.Done():
			return nil
		case w := <-webhooks:
			chats, err := b.chats.List()
			if err != nil {
				level.Error(b.logger).Log("msg", "failed to get chat list from store", "err", err)
				continue
			}

			data := &template.Data{
				Receiver:          w.Receiver,
				Status:            w.Status,
				Alerts:            w.Alerts,
				GroupLabels:       w.GroupLabels,
				CommonLabels:      w.CommonLabels,
				CommonAnnotations: w.CommonAnnotations,
				ExternalURL:       w.ExternalURL,
			}

			out, err := b.templates.ExecuteHTMLString(`{{ template "telegram.default" . }}`, data)
			if err != nil {
				level.Warn(b.logger).Log("msg", "failed to template alerts", "err", err)
				continue
			}

			for _, chat := range chats {
				b.telegram.SendMessage(chat, out, &telebot.SendOptions{ParseMode: telebot.ModeHTML})
			}
		}
	}
}

func (b *Bot) handleStart(message telebot.Message) {
	if err := b.chats.Add(message.Chat); err != nil {
		level.Warn(b.logger).Log("msg", "failed to add chat to chat store", "err", err)
		b.telegram.SendMessage(message.Chat, "I can't add this chat to the subscribers list.", nil)
		return
	}

	b.telegram.SendMessage(message.Chat, fmt.Sprintf(responseStart, message.Sender.FirstName), nil)
	level.Info(b.logger).Log(
		"user subscribed",
		"username", message.Sender.Username,
		"user_id", message.Sender.ID,
	)
}

func (b *Bot) handleStop(message telebot.Message) {
	if err := b.chats.Remove(message.Chat); err != nil {
		level.Warn(b.logger).Log("msg", "failed to remove chat from chat store", "err", err)
		b.telegram.SendMessage(message.Chat, "I can't remove this chat from the subscribers list.", nil)
		return
	}

	b.telegram.SendMessage(message.Chat, fmt.Sprintf(responseStop, message.Sender.FirstName), nil)
	level.Info(b.logger).Log(
		"user unsubscribed",
		"username", message.Sender.Username,
		"user_id", message.Sender.ID,
	)
}

func (b *Bot) handleHelp(message telebot.Message) {
	b.telegram.SendMessage(message.Chat, responseHelp, nil)
}

func (b *Bot) handleChats(message telebot.Message) {
	chats, err := b.chats.List()
	if err != nil {
		level.Warn(b.logger).Log("msg", "failed to list chats from chat store", "err", err)
		b.telegram.SendMessage(message.Chat, "I can't list the subscribed chats.", nil)
		return
	}

	list := ""
	for _, chat := range chats {
		if chat.IsGroupChat() {
			list = list + fmt.Sprintf("@%s\n", chat.Title)
		} else {
			list = list + fmt.Sprintf("@%s\n", chat.Username)
		}
	}

	b.telegram.SendMessage(message.Chat, "Currently these chat have subscribed:\n"+list, nil)
}

func (b *Bot) handleStatus(message telebot.Message) {
	s, err := alertmanager.Status(b.logger, b.alertmanager.String())
	if err != nil {
		level.Warn(b.logger).Log("msg", "failed to get status", "err", err)
		b.telegram.SendMessage(message.Chat, fmt.Sprintf("failed to get status... %v", err), nil)
		return
	}

	uptime := durafmt.Parse(time.Since(s.Data.Uptime))
	uptimeBot := durafmt.Parse(time.Since(b.startTime))

	b.telegram.SendMessage(
		message.Chat,
		fmt.Sprintf(
			"*AlertManager*\nVersion: %s\nUptime: %s\n*AlertManager Bot*\nVersion: %s\nUptime: %s",
			s.Data.VersionInfo.Version,
			uptime,
			b.revision,
			uptimeBot,
		),
		&telebot.SendOptions{ParseMode: telebot.ModeMarkdown},
	)
}

func (b *Bot) handleAlerts(message telebot.Message) {
	alerts, err := alertmanager.ListAlerts(b.logger, b.alertmanager.String())
	if err != nil {
		b.telegram.SendMessage(message.Chat, fmt.Sprintf("failed to list alerts... %v", err), nil)
		return
	}

	if len(alerts) == 0 {
		b.telegram.SendMessage(message.Chat, "No alerts right now! 🎉", nil)
		return
	}

	out, err := b.tmplAlerts(alerts...)
	if err != nil {
		return
	}

	err = b.telegram.SendMessage(message.Chat, out, &telebot.SendOptions{
		ParseMode: telebot.ModeHTML,
	})
	if err != nil {
		level.Warn(b.logger).Log("msg", "failed to send message", "err", err)
	}
}

func (b *Bot) handleSilences(message telebot.Message) {
	silences, err := alertmanager.ListSilences(b.logger, b.alertmanager.String())
	if err != nil {
		b.telegram.SendMessage(message.Chat, fmt.Sprintf("failed to list silences... %v", err), nil)
		return
	}

	if len(silences) == 0 {
		b.telegram.SendMessage(message.Chat, "No silences right now.", nil)
		return
	}

	var out string
	for _, silence := range silences {
		out = out + alertmanager.SilenceMessage(silence) + "\n"
	}

	b.telegram.SendMessage(message.Chat, out, &telebot.SendOptions{ParseMode: telebot.ModeMarkdown})
}

func (b *Bot) tmplAlerts(alerts ...*types.Alert) (string, error) {
	data := b.templates.Data("default", nil, alerts...)

	out, err := b.templates.ExecuteHTMLString(`{{ template "telegram.default" . }}`, data)
	if err != nil {
		return "", err
	}

	return out, nil
}
<|endoftext|>


<commit_before>package types

// Chart Type contains string representations of chart types.
const (
	ChartBar           = "bar"
	ChartBar3D         = "bar3D"
	ChartBoxPlot       = "boxplot"
	ChartCartesian3D   = "cartesian3D"
	ChartEffectScatter = "effectScatter"
	ChartFunnel        = "funnel"
	ChartGauge         = "gauge"
	ChartGeo           = "geo"
	ChartGraph         = "graph"
	ChartHeatMap       = "heatmap"
	ChartKline         = "candlestick"
	ChartLine          = "line"
	ChartLine3D        = "line3d"
	ChartLiquid        = "liquidFill"
	ChartMap           = "map"
	ChartParallel      = "parallel"
	ChartPie           = "pie"
	ChartRadar         = "radar"
	ChartSankey        = "sankey"
	ChartScatter       = "scatter"
	ChartScatter3D     = "scatter3D"
	ChartSurface3D     = "surface"
	ChartThemeRiver    = "themeRiver"
	ChartWordCloud     = "wordCloud"
)
<commit_msg>Fix the wrong type name of Line3D. (#99)<commit_after>package types

// Chart Type contains string representations of chart types.
const (
	ChartBar           = "bar"
	ChartBar3D         = "bar3D"
	ChartBoxPlot       = "boxplot"
	ChartCartesian3D   = "cartesian3D"
	ChartEffectScatter = "effectScatter"
	ChartFunnel        = "funnel"
	ChartGauge         = "gauge"
	ChartGeo           = "geo"
	ChartGraph         = "graph"
	ChartHeatMap       = "heatmap"
	ChartKline         = "candlestick"
	ChartLine          = "line"
	ChartLine3D        = "line3D"
	ChartLiquid        = "liquidFill"
	ChartMap           = "map"
	ChartParallel      = "parallel"
	ChartPie           = "pie"
	ChartRadar         = "radar"
	ChartSankey        = "sankey"
	ChartScatter       = "scatter"
	ChartScatter3D     = "scatter3D"
	ChartSurface3D     = "surface"
	ChartThemeRiver    = "themeRiver"
	ChartWordCloud     = "wordCloud"
)
<|endoftext|>


<commit_before>package types

import (
	"encoding/json"

	"github.com/go-pg/pg/v10/internal/pool"
	"github.com/go-pg/pg/v10/pgjson"
)

const (
	pgBool = 16

	pgInt2 = 21
	pgInt4 = 23
	pgInt8 = 20

	pgFloat4 = 700
	pgFloat8 = 701

	pgText    = 25
	pgVarchar = 1043
	pgBytea   = 17
	pgJSON    = 114
	pgJSONB   = 3802

	pgTimestamp   = 1114
	pgTimestamptz = 1184

	// pgInt2Array = 1005
	pgInt8Array   = 1016
	pgFloat8Array = 1022
	pgStringArray = 1009

	pgUUID = 2950
)

type ColumnInfo = pool.ColumnInfo

type RawValue struct {
	Type  int32
	Value string
}

func (v RawValue) AppendValue(b []byte, flags int) ([]byte, error) {
	return AppendString(b, v.Value, flags), nil
}

func (v RawValue) MarshalJSON() ([]byte, error) {
	return pgjson.Marshal(v.Value)
}

func ReadColumnValue(col ColumnInfo, rd Reader, n int) (interface{}, error) {
	switch col.DataType {
	case pgBool:
		return ScanBool(rd, n)

	case pgInt2:
		n, err := scanInt64(rd, n, 16)
		if err != nil {
			return nil, err
		}
		return int16(n), nil
	case pgInt4:
		n, err := scanInt64(rd, n, 32)
		if err != nil {
			return nil, err
		}
		return int32(n), nil
	case pgInt8:
		return ScanInt64(rd, n)

	case pgFloat4:
		return ScanFloat32(rd, n)
	case pgFloat8:
		return ScanFloat64(rd, n)

	case pgBytea:
		return ScanBytes(rd, n)
	case pgText, pgVarchar, pgUUID:
		return ScanString(rd, n)
	case pgJSON, pgJSONB:
		s, err := ScanString(rd, n)
		if err != nil {
			return nil, err
		}
		return json.RawMessage(s), nil

	case pgTimestamp:
		return ScanTime(rd, n)
	case pgTimestamptz:
		return ScanTime(rd, n)

	case pgInt8Array:
		return scanInt64Array(rd, n)
	case pgFloat8Array:
		return scanFloat64Array(rd, n)
	case pgStringArray:
		return scanStringArray(rd, n)

	default:
		s, err := ScanString(rd, n)
		if err != nil {
			return nil, err
		}
		return RawValue{
			Type:  col.DataType,
			Value: s,
		}, nil
	}
}
<commit_msg>int[] type to array conversion<commit_after>package types

import (
	"encoding/json"

	"github.com/go-pg/pg/v10/internal/pool"
	"github.com/go-pg/pg/v10/pgjson"
)

const (
	pgBool = 16

	pgInt2 = 21
	pgInt4 = 23
	pgInt8 = 20

	pgFloat4 = 700
	pgFloat8 = 701

	pgText    = 25
	pgVarchar = 1043
	pgBytea   = 17
	pgJSON    = 114
	pgJSONB   = 3802

	pgTimestamp   = 1114
	pgTimestamptz = 1184

	// pgInt2Array = 1005
	pgInt32Array  = 1007
	pgInt8Array   = 1016
	pgFloat8Array = 1022
	pgStringArray = 1009

	pgUUID = 2950
)

type ColumnInfo = pool.ColumnInfo

type RawValue struct {
	Type  int32
	Value string
}

func (v RawValue) AppendValue(b []byte, flags int) ([]byte, error) {
	return AppendString(b, v.Value, flags), nil
}

func (v RawValue) MarshalJSON() ([]byte, error) {
	return pgjson.Marshal(v.Value)
}

func ReadColumnValue(col ColumnInfo, rd Reader, n int) (interface{}, error) {
	switch col.DataType {
	case pgBool:
		return ScanBool(rd, n)

	case pgInt2:
		n, err := scanInt64(rd, n, 16)
		if err != nil {
			return nil, err
		}
		return int16(n), nil
	case pgInt4:
		n, err := scanInt64(rd, n, 32)
		if err != nil {
			return nil, err
		}
		return int32(n), nil
	case pgInt8:
		return ScanInt64(rd, n)

	case pgFloat4:
		return ScanFloat32(rd, n)
	case pgFloat8:
		return ScanFloat64(rd, n)

	case pgBytea:
		return ScanBytes(rd, n)
	case pgText, pgVarchar, pgUUID:
		return ScanString(rd, n)
	case pgJSON, pgJSONB:
		s, err := ScanString(rd, n)
		if err != nil {
			return nil, err
		}
		return json.RawMessage(s), nil

	case pgTimestamp:
		return ScanTime(rd, n)
	case pgTimestamptz:
		return ScanTime(rd, n)

	case pgInt32Array:
		return scanInt64Array(rd, n)
	case pgInt8Array:
		return scanInt64Array(rd, n)
	case pgFloat8Array:
		return scanFloat64Array(rd, n)
	case pgStringArray:
		return scanStringArray(rd, n)

	default:
		s, err := ScanString(rd, n)
		if err != nil {
			return nil, err
		}
		return RawValue{
			Type:  col.DataType,
			Value: s,
		}, nil
	}
}
<|endoftext|>


<commit_before>package validator

import (
	"errors"
	"reflect"
	"regexp"
	"strconv"
)

const (
	EmailPattern = `^[a-zA-Z0-9!#$%&\'*+\\/=?^_{|}~-]+(?:\.[a-zA-Z0-9!#$%&\'*+\\/=?^_{|}~-]+)*@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$`
	UrlPattern   = `((http|https|ftp){1}\:\/\/)?([a-zA-Z0-9\-\.]+[a-zA-Z0-9]+\.(net|cn|co|hk|tw|com|edu|gov|us|int|mil|org|int|mil|vg|uk|idv|tk|se|nz|nu|nl|ms|jp|jobs|it|ind|gen|firm|in|gs|fr|fm|eu|es|de|bz|be|at|am|ag|mx|asia|ws|xxx|tv|cc|ca|mobi|me|biz|arpa|info|name|pro|aero|coop|museum|ly|eg|mk)(:[a-zA-Z0-9]*)?\/?([a-zA-Z0-9\-\._\?\'\/\\\+&%\$#\=~])*)+`
)

type String struct {
	CoreValidate
}

func (self *String) Filter(f reflect.StructField, fv reflect.Value) bool {
	return f.Type.Kind() == reflect.String
}

func (self *String) Validate(f reflect.StructField, fv reflect.Value) (errs []error) {
	if "required" == f.Tag.Get("required") && "" == fv.String() {
		errs = append(errs, errors.New(f.Name+" cannot be blank"))
	}

	min := f.Tag.Get("min")
	if "" != min {
		min2, err := strconv.Atoi(min)
		if err != nil {
			errs = append(errs, err)
		}

		if min2 > fv.Len() {
			errs = append(errs, errors.New(f.Name+": min err"))
		}
	}

	max := f.Tag.Get("max")
	if "" != max {
		max2, err := strconv.Atoi(max)
		if err != nil {
			errs = append(errs, err)
		}

		if max2 < fv.Len() {
			errs = append(errs, errors.New(f.Name+": max err"))
		}
	}

	is := f.Tag.Get("is")
	if "" != is {
		switch is {
		case "email":
			if ok, err := regexp.MatchString(EmailPattern, fv.String()); !ok {
				errs = append(errs, errors.New(f.Name+": is not a valid email"))
			} else if err != nil {
				errs = append(errs, err)
			}
		case "url":
			if ok, err := regexp.MatchString(UrlPattern, fv.String()); !ok {
				errs = append(errs, errors.New(f.Name+": is not a valid url"))
			} else if err != nil {
				errs = append(errs, err)
			}
		}
	}

	return
}
<commit_msg>use is package<commit_after>package validator

import (
	"errors"
	"reflect"
	"strconv"

	"github.com/elsonwu/i18n"
	"github.com/elsonwu/is"
)

type String struct {
	CoreValidate
}

func (self *String) Filter(f reflect.StructField, fv reflect.Value) bool {
	return f.Type.Kind() == reflect.String
}

func (self *String) Validate(f reflect.StructField, fv reflect.Value) (errs []error) {
	if "required" == f.Tag.Get("required") && "" == fv.String() {
		errs = append(errs, errors.New(i18n.T("%s cannot be blank", f.Name)))
	}

	min := f.Tag.Get("min")
	if "" != min {
		min2, err := strconv.Atoi(min)
		if err != nil {
			errs = append(errs, err)
		}

		if min2 > fv.Len() {
			errs = append(errs, errors.New(i18n.T("%s min err", f.Name)))
		}
	}

	max := f.Tag.Get("max")
	if "" != max {
		max2, err := strconv.Atoi(max)
		if err != nil {
			errs = append(errs, err)
		}

		if max2 < fv.Len() {
			errs = append(errs, errors.New(i18n.T("%s max err", f.Name)))
		}
	}

	isv := f.Tag.Get("is")
	if "" != isv {
		switch isv {
		case "email":
			if !is.Email(fv.String()) {
				errs = append(errs, errors.New(i18n.T("%s is not a valid email", f.Name)))
			}
		case "url":
			if !is.Url(fv.String()) {
				errs = append(errs, errors.New(i18n.T("%s is not a valid url", f.Name)))
			}
		}
	}

	return
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2013 Zhen, LLC. http://zhen.io. All rights reserved.
 * Use of this source code is governed by the Apache 2.0 license.
 *
 */

package samples

import (
	"testing"
	"log"
	"os"
	"bufio"
	"encoding/binary"
	"compress/gzip"
	"strconv"
	"runtime"
	"github.com/reducedb/encoding"
	"github.com/reducedb/encoding/composition"
	"github.com/reducedb/encoding/fastpfor"
	"github.com/reducedb/encoding/bp32"
	"github.com/reducedb/encoding/variablebyte"
)

func TestEncoding(t *testing.T) {
	testEncodingWithFile("ts.txt.gz", t)
	testEncodingWithFile("srcip.txt.gz", t)
	testEncodingWithFile("dstip.txt.gz", t)
	testEncodingWithFile("latency.txt.gz", t)
}

func TestPprofEncoding(t *testing.T) {
	data, err := readFileOfIntegers("ts.txt.gz")
	if err == nil {
		log.Printf("encoding/testEncodingWithFile: Read %d integers (%d bytes) from ts.txt.gz.\n", len(data), len(data)*4)
	} else {
		log.Printf("encoding/testEncodingWithFile: Error opening ts.txt.gz: %v\n", err)
	}

	encoding.TestCodecPprof(composition.NewComposition(bp32.NewZigZagBP32(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)
}

func testEncodingWithFile(path string, t *testing.T) {
	data, err := readFileOfIntegers(path)
	if err == nil {
		log.Printf("encoding/testEncodingWithFile: Read %d integers (%d bytes) from %s.\n", len(data), len(data)*4, path)
	} else {
		log.Printf("encoding/testEncodingWithFile: Error opening ts.txt.gz: %v\n", err)
	}

	log.Printf("encoding/testEncodingWithFile: Testing comprssion FastPFOR+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(fastpfor.New(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithFile: Testing comprssion ZigZag BP32+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(bp32.NewZigZagBP32(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithFile: Testing comprssion Delta BP32+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(bp32.NewDeltaBP32(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithFile: Testing comprssion Delta VariableByte\n")
	encoding.TestCodec(variablebyte.NewDeltaVariableByte(), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithFile: Testing comprssion BP32+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(bp32.NewBP32(), variablebyte.NewVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithFile: Testing comprssion VariableByte\n")
	encoding.TestCodec(variablebyte.NewVariableByte(), data, []int{len(data)}, t)

	b := make([]byte, len(data)*4)
	for i := 0; i < len(data); i++ {
		binary.LittleEndian.PutUint32(b[i*4:], uint32(data[i]))
	}

	encoding.RunTestGzip(b, t)
	encoding.RunTestLZW(b, t)
}

func readFileOfIntegers(path string) ([]int32, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	gunzip, err := gzip.NewReader(file)
	if err != nil {
		log.Printf("encoding/readFileOfIntegers: Error opening gzip reader: %v\n", err)
		return nil, err
	}

	result := make([]int32, 0, 50000000)
	scanner := bufio.NewScanner(gunzip)
	for scanner.Scan() {
		i, e := strconv.ParseUint(scanner.Text(), 10, 32)
		if e != nil {
			log.Printf("encoding/readFileOfIntegers: Error reading from %s. %v\n", path, e)
		} else {
			result = append(result, int32(i))
		}
	}

	// Run the garbage collector to get rid of all the strings that's been allocated
	// during the file read
	runtime.GC()

	return result, scanner.Err()
}
<commit_msg>Added some additional tests.<commit_after>/*
 * Copyright (c) 2013 Zhen, LLC. http://zhen.io. All rights reserved.
 * Use of this source code is governed by the Apache 2.0 license.
 *
 */

package samples

import (
	"testing"
	"log"
	"os"
	"bufio"
	"encoding/binary"
	"compress/gzip"
	"strconv"
	"runtime"
	"github.com/reducedb/encoding"
	"github.com/reducedb/encoding/composition"
	"github.com/reducedb/encoding/fastpfor"
	"github.com/reducedb/encoding/bp32"
	"github.com/reducedb/encoding/variablebyte"
)

func TestEncodingGenerateClustered(t *testing.T) {
	var size int = 10000000

	log.Printf("bp32/init: generating %d int32s\n", size)
	data := encoding.GenerateClustered(size, size*2)
	testEncodingWithData(data, t)
}

func TestEncodingGenerateUniform(t *testing.T) {
	var size int = 10000000

	log.Printf("bp32/init: generating %d int32s\n", size)
	data := encoding.GenerateUniform(size, size*2)
	testEncodingWithData(data, t)
}

func TestEncodingFiles(t *testing.T) {
	for _, f := range []string{"ts.txt.gz", "scrip.txt.gz", "dstip.txt.gz", "latency.txt.gz"} {
		data, err := readFileOfIntegers(f)
		if err == nil {
			log.Printf("encoding/testEncodingWithFile: Read %d integers (%d bytes) from %s.\n", len(data), len(data)*4, f)
			testEncodingWithData(data, t)
		} else {
			log.Printf("encoding/testEncodingWithFile: Error opening ts.txt.gz: %v\n", err)
		}
	}
}

func TestEncodingPprof(t *testing.T) {
	data, err := readFileOfIntegers("ts.txt.gz")
	if err == nil {
		log.Printf("encoding/testEncodingWithFile: Read %d integers (%d bytes) from ts.txt.gz.\n", len(data), len(data)*4)
	} else {
		log.Printf("encoding/testEncodingWithFile: Error opening ts.txt.gz: %v\n", err)
	}

	encoding.TestCodecPprof(composition.NewComposition(bp32.NewZigZagBP32(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)
}

func testEncodingWithData(data []int32, t *testing.T) {
	log.Printf("encoding/testEncodingWithData: Testing comprssion FastPFOR+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(fastpfor.New(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithData: Testing comprssion ZigZag BP32+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(bp32.NewZigZagBP32(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithData: Testing comprssion Delta BP32+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(bp32.NewDeltaBP32(), variablebyte.NewDeltaVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithData: Testing comprssion Delta VariableByte\n")
	encoding.TestCodec(variablebyte.NewDeltaVariableByte(), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithData: Testing comprssion BP32+VariableByte\n")
	encoding.TestCodec(composition.NewComposition(bp32.NewBP32(), variablebyte.NewVariableByte()), data, []int{len(data)}, t)

	log.Printf("encoding/testEncodingWithData: Testing comprssion VariableByte\n")
	encoding.TestCodec(variablebyte.NewVariableByte(), data, []int{len(data)}, t)

	b := make([]byte, len(data)*4)
	for i := 0; i < len(data); i++ {
		binary.LittleEndian.PutUint32(b[i*4:], uint32(data[i]))
	}

	encoding.RunTestGzip(b, t)
	encoding.RunTestLZW(b, t)
}

func readFileOfIntegers(path string) ([]int32, error) {
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	gunzip, err := gzip.NewReader(file)
	if err != nil {
		log.Printf("encoding/readFileOfIntegers: Error opening gzip reader: %v\n", err)
		return nil, err
	}

	result := make([]int32, 0, 50000000)
	scanner := bufio.NewScanner(gunzip)
	for scanner.Scan() {
		i, e := strconv.ParseUint(scanner.Text(), 10, 32)
		if e != nil {
			log.Printf("encoding/readFileOfIntegers: Error reading from %s. %v\n", path, e)
		} else {
			result = append(result, int32(i))
		}
	}

	// Run the garbage collector to get rid of all the strings that's been allocated
	// during the file read
	runtime.GC()

	return result, scanner.Err()
}
<|endoftext|>


<commit_before>package midware

import (
	"github.com/ardanlabs/kit/cfg"
	"github.com/ardanlabs/kit/db"
	"github.com/ardanlabs/kit/log"
	"github.com/ardanlabs/kit/web/app"
)

// cfgDB config environmental variables.
const cfgMongoDB = "MONGO_DB"

// Mongo handles session management.
func Mongo(h app.Handler) app.Handler {

	// Check if mongodb is configured.
	dbName, err := cfg.String(cfgMongoDB)
	if err != nil {
		return func(c *app.Context) error {
			log.Dev(c.SessionID, "Mongo", "******> Mongo Not Configured")
			return h(c)
		}
	}

	// Wrap the handlers inside a session copy/close.
	return func(c *app.Context) error {
		mgoDB, err := db.NewMGO("Mongo", dbName)
		if err != nil {
			log.Error(c.SessionID, "Mongo", err, "Method[%s] URL[%s] RADDR[%s]", c.Request.Method, c.Request.URL.Path, c.Request.RemoteAddr)
			return app.ErrDBNotConfigured
		}

		log.Dev(c.SessionID, "Mongo", "******> SET MONGODB CONNECTION")
		c.DB = mgoDB

		h(c)

		mgoDB.CloseMGO("Mongo")
		log.Dev(c.SessionID, "Mongo", "******> RELEASED MONGODB CONNECTION")

		return nil
	}
}
<commit_msg>Updated dev level logging.<commit_after>package midware

import (
	"github.com/ardanlabs/kit/cfg"
	"github.com/ardanlabs/kit/db"
	"github.com/ardanlabs/kit/log"
	"github.com/ardanlabs/kit/web/app"
)

// cfgDB config environmental variables.
const cfgMongoDB = "MONGO_DB"

// Mongo handles session management.
func Mongo(h app.Handler) app.Handler {

	// Check if mongodb is configured.
	dbName, err := cfg.String(cfgMongoDB)
	if err != nil {
		return func(c *app.Context) error {
			log.Dev(c.SessionID, "Mongo", "******> Mongo Not Configured")
			return h(c)
		}
	}

	// Wrap the handlers inside a session copy/close.
	return func(c *app.Context) error {
		mgoDB, err := db.NewMGO("Mongo", dbName)
		if err != nil {
			log.Error(c.SessionID, "Mongo", err, "Method[%s] URL[%s] RADDR[%s]", c.Request.Method, c.Request.URL.Path, c.Request.RemoteAddr)
			return app.ErrDBNotConfigured
		}

		log.Dev(c.SessionID, "Mongo", "******> Capture Mongo Session")
		c.DB = mgoDB
		defer func() {
			log.Dev(c.SessionID, "Mongo", "******> Release Mongo Session")
			mgoDB.CloseMGO("Mongo")
		}()

		return h(c)
	}
}
<|endoftext|>


<commit_before><commit_msg>Moved ThemeProps migration to correctly take place in 3.2 -> 3.3 migration (#4054)<commit_after><|endoftext|>


<commit_before><commit_msg>Add postForm tests<commit_after><|endoftext|>


<commit_before>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package validation

import (
	"github.com/go-openapi/spec"
	"github.com/go-openapi/validate"

	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)

// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
// CustomResource is a JSON data structure.
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
	result := validator.Validate(customResource)
	if result.AsError() != nil {
		return result.AsError()
	}
	return nil
}

// ConvertToOpenAPITypes is used to convert internal types to go-openapi types.
func ConvertToOpenAPITypes(in *apiextensions.CustomResourceDefinition, out *spec.Schema) error {
	if in.Spec.Validation != nil {
		if err := convertJSONSchemaProps(in.Spec.Validation.OpenAPIV3Schema, out); err != nil {
			return err
		}
	}

	return nil
}

func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
	if in == nil {
		return nil
	}

	out.ID = in.ID
	out.Schema = spec.SchemaURL(in.Schema)
	out.Description = in.Description
	if in.Type != "" {
		out.Type = spec.StringOrArray([]string{in.Type})
	}
	out.Format = in.Format
	out.Title = in.Title
	out.ExclusiveMaximum = in.ExclusiveMaximum
	out.Minimum = in.Minimum
	out.ExclusiveMinimum = in.ExclusiveMinimum
	out.MaxLength = in.MaxLength
	out.MinLength = in.MinLength
	out.Pattern = in.Pattern
	out.MaxItems = in.MaxItems
	out.MinItems = in.MinItems
	out.UniqueItems = in.UniqueItems
	out.MultipleOf = in.MultipleOf
	out.MaxProperties = in.MaxProperties
	out.MinProperties = in.MinProperties
	out.Required = in.Required

	if in.Default != nil {
		out.Default = *(in.Default)
	}
	if in.Example != nil {
		out.Example = *(in.Example)
	}

	out.Enum = make([]interface{}, len(in.Enum))
	for k, v := range in.Enum {
		out.Enum[k] = v
	}

	if err := convertJSONSchemaPropsOrArray(in.Items, out.Items); err != nil {
		return err
	}
	if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf); err != nil {
		return err
	}
	if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf); err != nil {
		return err
	}
	if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf); err != nil {
		return err
	}
	if err := convertJSONSchemaProps(in.Not, out.Not); err != nil {
		return err
	}

	var err error
	out.Properties, err = convertMapOfJSONSchemaProps(in.Properties)
	if err != nil {
		return err
	}

	out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties)
	if err != nil {
		return err
	}

	if in.Ref != nil {
		out.Ref, err = spec.NewRef(*in.Ref)
		if err != nil {
			return err
		}
	}

	if err := convertJSONSchemaPropsorBool(in.AdditionalProperties, out.AdditionalProperties); err != nil {
		return err
	}

	if err := convertJSONSchemaPropsorBool(in.AdditionalItems, out.AdditionalItems); err != nil {
		return err
	}

	if err := convertJSONSchemaDependencies(in.Dependencies, out.Dependencies); err != nil {
		return err
	}

	out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions)
	if err != nil {
		return err
	}

	if in.ExternalDocs != nil {
		out.ExternalDocs = &spec.ExternalDocumentation{}
		out.ExternalDocs.Description = in.ExternalDocs.Description
		out.ExternalDocs.URL = in.ExternalDocs.URL
	}

	return nil
}

func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema) error {
	if in != nil {
		for _, jsonSchemaProps := range *in {
			schema := spec.Schema{}
			if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
				return err
			}
			*out = append(*out, schema)
		}
	}
	return nil
}

func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (map[string]spec.Schema, error) {
	out := make(map[string]spec.Schema)
	if len(in) != 0 {
		for k, jsonSchemaProps := range in {
			schema := spec.Schema{}
			if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
				return nil, err
			}
			out[k] = schema
		}
	}
	return out, nil
}

func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray) error {
	if in != nil {
		out.Schema = &spec.Schema{}
		if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil {
			return err
		}
	}
	return nil
}

func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool) error {
	if in != nil {
		out = &spec.SchemaOrBool{}
		out.Allows = in.Allows
		out.Schema = &spec.Schema{}
		if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil {
			return err
		}
	}
	return nil
}

func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray) error {
	if in != nil {
		out.Property = in.Property
		out.Schema = &spec.Schema{}
		if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil {
			return err
		}
	}
	return nil
}

func convertJSONSchemaDependencies(in apiextensions.JSONSchemaDependencies, out spec.Dependencies) error {
	if in != nil {
		for k, v := range in {
			schemaOrArray := spec.SchemaOrStringArray{}
			if err := convertJSONSchemaPropsOrStringArray(&v, &schemaOrArray); err != nil {
				return err
			}
			out[k] = schemaOrArray
		}
	}
	return nil
}
<commit_msg>apiextensions: add maximum for validation<commit_after>/*
Copyright 2017 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package validation

import (
	"github.com/go-openapi/spec"
	"github.com/go-openapi/validate"

	"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
)

// ValidateCustomResource validates the Custom Resource against the schema in the CustomResourceDefinition.
// CustomResource is a JSON data structure.
func ValidateCustomResource(customResource interface{}, validator *validate.SchemaValidator) error {
	result := validator.Validate(customResource)
	if result.AsError() != nil {
		return result.AsError()
	}
	return nil
}

// ConvertToOpenAPITypes is used to convert internal types to go-openapi types.
func ConvertToOpenAPITypes(in *apiextensions.CustomResourceDefinition, out *spec.Schema) error {
	if in.Spec.Validation != nil {
		if err := convertJSONSchemaProps(in.Spec.Validation.OpenAPIV3Schema, out); err != nil {
			return err
		}
	}

	return nil
}

func convertJSONSchemaProps(in *apiextensions.JSONSchemaProps, out *spec.Schema) error {
	if in == nil {
		return nil
	}

	out.ID = in.ID
	out.Schema = spec.SchemaURL(in.Schema)
	out.Description = in.Description
	if in.Type != "" {
		out.Type = spec.StringOrArray([]string{in.Type})
	}
	out.Format = in.Format
	out.Title = in.Title
	out.Maximum = in.Maximum
	out.ExclusiveMaximum = in.ExclusiveMaximum
	out.Minimum = in.Minimum
	out.ExclusiveMinimum = in.ExclusiveMinimum
	out.MaxLength = in.MaxLength
	out.MinLength = in.MinLength
	out.Pattern = in.Pattern
	out.MaxItems = in.MaxItems
	out.MinItems = in.MinItems
	out.UniqueItems = in.UniqueItems
	out.MultipleOf = in.MultipleOf
	out.MaxProperties = in.MaxProperties
	out.MinProperties = in.MinProperties
	out.Required = in.Required

	if in.Default != nil {
		out.Default = *(in.Default)
	}
	if in.Example != nil {
		out.Example = *(in.Example)
	}

	out.Enum = make([]interface{}, len(in.Enum))
	for k, v := range in.Enum {
		out.Enum[k] = v
	}

	if err := convertJSONSchemaPropsOrArray(in.Items, out.Items); err != nil {
		return err
	}
	if err := convertSliceOfJSONSchemaProps(&in.AllOf, &out.AllOf); err != nil {
		return err
	}
	if err := convertSliceOfJSONSchemaProps(&in.OneOf, &out.OneOf); err != nil {
		return err
	}
	if err := convertSliceOfJSONSchemaProps(&in.AnyOf, &out.AnyOf); err != nil {
		return err
	}
	if err := convertJSONSchemaProps(in.Not, out.Not); err != nil {
		return err
	}

	var err error
	out.Properties, err = convertMapOfJSONSchemaProps(in.Properties)
	if err != nil {
		return err
	}

	out.PatternProperties, err = convertMapOfJSONSchemaProps(in.PatternProperties)
	if err != nil {
		return err
	}

	if in.Ref != nil {
		out.Ref, err = spec.NewRef(*in.Ref)
		if err != nil {
			return err
		}
	}

	if err := convertJSONSchemaPropsorBool(in.AdditionalProperties, out.AdditionalProperties); err != nil {
		return err
	}

	if err := convertJSONSchemaPropsorBool(in.AdditionalItems, out.AdditionalItems); err != nil {
		return err
	}

	if err := convertJSONSchemaDependencies(in.Dependencies, out.Dependencies); err != nil {
		return err
	}

	out.Definitions, err = convertMapOfJSONSchemaProps(in.Definitions)
	if err != nil {
		return err
	}

	if in.ExternalDocs != nil {
		out.ExternalDocs = &spec.ExternalDocumentation{}
		out.ExternalDocs.Description = in.ExternalDocs.Description
		out.ExternalDocs.URL = in.ExternalDocs.URL
	}

	return nil
}

func convertSliceOfJSONSchemaProps(in *[]apiextensions.JSONSchemaProps, out *[]spec.Schema) error {
	if in != nil {
		for _, jsonSchemaProps := range *in {
			schema := spec.Schema{}
			if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
				return err
			}
			*out = append(*out, schema)
		}
	}
	return nil
}

func convertMapOfJSONSchemaProps(in map[string]apiextensions.JSONSchemaProps) (map[string]spec.Schema, error) {
	out := make(map[string]spec.Schema)
	if len(in) != 0 {
		for k, jsonSchemaProps := range in {
			schema := spec.Schema{}
			if err := convertJSONSchemaProps(&jsonSchemaProps, &schema); err != nil {
				return nil, err
			}
			out[k] = schema
		}
	}
	return out, nil
}

func convertJSONSchemaPropsOrArray(in *apiextensions.JSONSchemaPropsOrArray, out *spec.SchemaOrArray) error {
	if in != nil {
		out.Schema = &spec.Schema{}
		if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil {
			return err
		}
	}
	return nil
}

func convertJSONSchemaPropsorBool(in *apiextensions.JSONSchemaPropsOrBool, out *spec.SchemaOrBool) error {
	if in != nil {
		out = &spec.SchemaOrBool{}
		out.Allows = in.Allows
		out.Schema = &spec.Schema{}
		if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil {
			return err
		}
	}
	return nil
}

func convertJSONSchemaPropsOrStringArray(in *apiextensions.JSONSchemaPropsOrStringArray, out *spec.SchemaOrStringArray) error {
	if in != nil {
		out.Property = in.Property
		out.Schema = &spec.Schema{}
		if err := convertJSONSchemaProps(in.Schema, out.Schema); err != nil {
			return err
		}
	}
	return nil
}

func convertJSONSchemaDependencies(in apiextensions.JSONSchemaDependencies, out spec.Dependencies) error {
	if in != nil {
		for k, v := range in {
			schemaOrArray := spec.SchemaOrStringArray{}
			if err := convertJSONSchemaPropsOrStringArray(&v, &schemaOrArray); err != nil {
				return err
			}
			out[k] = schemaOrArray
		}
	}
	return nil
}
<|endoftext|>