<commit_before>// +build e2e

/*
Copyright 2020 The Knative 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 ingress

import (
	"fmt"
	"math"
	"net/http"
	"testing"

	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/apimachinery/pkg/util/sets"
	"knative.dev/serving/pkg/apis/networking"
	"knative.dev/serving/pkg/apis/networking/v1alpha1"
	"knative.dev/serving/test"
)

func TestVisibility(t *testing.T) {
	t.Parallel()
	clients := test.Setup(t)

	// Create the private backend
	name, port, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
	defer cancel()

	privateServiceName := test.ObjectNameForTest(t)
	privateHostName := privateServiceName + "." + test.ServingNamespace + ".svc.cluster.local"
	ingress, client, cancel := CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{privateHostName},
			Visibility: v1alpha1.IngressVisibilityClusterLocal,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      name,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(port),
						},
					}},
				}},
			},
		}},
	})
	defer cancel()

	// Ensure the service is not publicly accessible
	RuntimeRequestWithExpectations(t, client, "http://"+privateHostName, []ResponseExpectation{StatusCodeExpectation(sets.NewInt(http.StatusNotFound))}, true)

	loadbalancerAddress := ingress.Status.PrivateLoadBalancer.Ingress[0].DomainInternal
	proxyName, proxyPort, cancel := CreateProxyService(t, clients, privateHostName, loadbalancerAddress)
	defer cancel()

	publicHostName := "publicproxy.example.com"
	_, client, cancel = CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{publicHostName},
			Visibility: v1alpha1.IngressVisibilityExternalIP,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      proxyName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(proxyPort),
						},
					}},
				}},
			},
		}},
	})
	defer cancel()

	// Ensure the service is accessible from within the cluster.
	RuntimeRequest(t, client, "http://"+publicHostName)
}

func TestVisibilitySplit(t *testing.T) {
	t.Parallel()
	clients := test.Setup(t)

	// Use a post-split injected header to establish which split we are sending traffic to.
	const headerName = "Foo-Bar-Baz"

	backends := make([]v1alpha1.IngressBackendSplit, 0, 10)
	weights := make(map[string]float64, len(backends))

	// Double the percentage of the split each iteration until it would overflow, and then
	// give the last route the remainder.
	percent, total := 1, 0
	for i := 0; i < 10; i++ {
		name, port, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
		defer cancel()
		backends = append(backends, v1alpha1.IngressBackendSplit{
			IngressBackend: v1alpha1.IngressBackend{
				ServiceName:      name,
				ServiceNamespace: test.ServingNamespace,
				ServicePort:      intstr.FromInt(port),
			},
			// Append different headers to each split, which lets us identify
			// which backend we hit.
			AppendHeaders: map[string]string{
				headerName: name,
			},
			Percent: percent,
		})
		weights[name] = float64(percent)

		total += percent
		percent *= 2
		// Cap the final non-zero bucket so that we total 100%
		// After that, this will zero out remaining buckets.
		if total+percent > 100 {
			percent = 100 - total
		}
	}

	name := test.ObjectNameForTest(t)

	// Create a simple Ingress over the 10 Services.
	privateHostName := fmt.Sprintf("%s.%s.%s", name, test.ServingNamespace, "svc.cluster.local")
	localIngress, client, cancel := CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{privateHostName},
			Visibility: v1alpha1.IngressVisibilityClusterLocal,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: backends,
				}},
			},
		}},
	})
	defer cancel()

	// Ensure we can't connect to the private resources
	RuntimeRequestWithExpectations(t, client, "http://"+privateHostName, []ResponseExpectation{StatusCodeExpectation(sets.NewInt(http.StatusNotFound))}, true)

	loadbalancerAddress := localIngress.Status.PrivateLoadBalancer.Ingress[0].DomainInternal
	proxyName, proxyPort, cancel := CreateProxyService(t, clients, privateHostName, loadbalancerAddress)
	defer cancel()

	publicHostName := fmt.Sprintf("%s.%s", name, "example.com")
	_, client, cancel = CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{publicHostName},
			Visibility: v1alpha1.IngressVisibilityExternalIP,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      proxyName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(proxyPort),
						},
					}},
				}},
			},
		}},
	})
	defer cancel()

	// Create a large enough population of requests that we can reasonably assess how
	// well the Ingress respected the percentage split.
	seen := make(map[string]float64, len(backends))

	const (
		// The total number of requests to make (as a float to avoid conversions in later computations).
		totalRequests = 1000.0
		// The increment to make for each request, so that the values of seen reflect the
		// percentage of the total number of requests we are making.
		increment = 100.0 / totalRequests
		// Allow the Ingress to be within 5% of the configured value.
		margin = 5.0
	)
	for i := 0.0; i < totalRequests; i++ {
		ri := RuntimeRequest(t, client, "http://"+publicHostName)
		if ri == nil {
			continue
		}
		seen[ri.Request.Headers.Get(headerName)] += increment
	}

	for name, want := range weights {
		got := seen[name]
		switch {
		case want == 0.0 && got > 0.0:
			// For 0% targets, we have tighter requirements.
			t.Errorf("Target %q received traffic, wanted none (0%% target).", name)
		case math.Abs(got-want) > margin:
			t.Errorf("Target %q received %f%%, wanted %f +/- %f", name, got, want, margin)
		}
	}
}
<commit_msg>Add ingress conformance test for visibility + paths (#7666)<commit_after>// +build e2e

/*
Copyright 2020 The Knative 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 ingress

import (
	"fmt"
	"math"
	"net/http"
	"testing"

	"k8s.io/apimachinery/pkg/util/intstr"
	"k8s.io/apimachinery/pkg/util/sets"
	"knative.dev/serving/pkg/apis/networking"
	"knative.dev/serving/pkg/apis/networking/v1alpha1"
	"knative.dev/serving/test"
)

func TestVisibility(t *testing.T) {
	t.Parallel()
	clients := test.Setup(t)

	// Create the private backend
	name, port, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
	defer cancel()

	privateServiceName := test.ObjectNameForTest(t)
	privateHostName := privateServiceName + "." + test.ServingNamespace + ".svc.cluster.local"
	ingress, client, cancel := CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{privateHostName},
			Visibility: v1alpha1.IngressVisibilityClusterLocal,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      name,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(port),
						},
					}},
				}},
			},
		}},
	})
	defer cancel()

	// Ensure the service is not publicly accessible
	RuntimeRequestWithExpectations(t, client, "http://"+privateHostName, []ResponseExpectation{StatusCodeExpectation(sets.NewInt(http.StatusNotFound))}, true)

	loadbalancerAddress := ingress.Status.PrivateLoadBalancer.Ingress[0].DomainInternal
	proxyName, proxyPort, cancel := CreateProxyService(t, clients, privateHostName, loadbalancerAddress)
	defer cancel()

	publicHostName := "publicproxy.example.com"
	_, client, cancel = CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{publicHostName},
			Visibility: v1alpha1.IngressVisibilityExternalIP,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      proxyName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(proxyPort),
						},
					}},
				}},
			},
		}},
	})
	defer cancel()

	// Ensure the service is accessible from within the cluster.
	RuntimeRequest(t, client, "http://"+publicHostName)
}

func TestVisibilitySplit(t *testing.T) {
	t.Parallel()
	clients := test.Setup(t)

	// Use a post-split injected header to establish which split we are sending traffic to.
	const headerName = "Foo-Bar-Baz"

	backends := make([]v1alpha1.IngressBackendSplit, 0, 10)
	weights := make(map[string]float64, len(backends))

	// Double the percentage of the split each iteration until it would overflow, and then
	// give the last route the remainder.
	percent, total := 1, 0
	for i := 0; i < 10; i++ {
		name, port, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
		defer cancel()
		backends = append(backends, v1alpha1.IngressBackendSplit{
			IngressBackend: v1alpha1.IngressBackend{
				ServiceName:      name,
				ServiceNamespace: test.ServingNamespace,
				ServicePort:      intstr.FromInt(port),
			},
			// Append different headers to each split, which lets us identify
			// which backend we hit.
			AppendHeaders: map[string]string{
				headerName: name,
			},
			Percent: percent,
		})
		weights[name] = float64(percent)

		total += percent
		percent *= 2
		// Cap the final non-zero bucket so that we total 100%
		// After that, this will zero out remaining buckets.
		if total+percent > 100 {
			percent = 100 - total
		}
	}

	name := test.ObjectNameForTest(t)

	// Create a simple Ingress over the 10 Services.
	privateHostName := fmt.Sprintf("%s.%s.%s", name, test.ServingNamespace, "svc.cluster.local")
	localIngress, client, cancel := CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{privateHostName},
			Visibility: v1alpha1.IngressVisibilityClusterLocal,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: backends,
				}},
			},
		}},
	})
	defer cancel()

	// Ensure we can't connect to the private resources
	RuntimeRequestWithExpectations(t, client, "http://"+privateHostName, []ResponseExpectation{StatusCodeExpectation(sets.NewInt(http.StatusNotFound))}, true)

	loadbalancerAddress := localIngress.Status.PrivateLoadBalancer.Ingress[0].DomainInternal
	proxyName, proxyPort, cancel := CreateProxyService(t, clients, privateHostName, loadbalancerAddress)
	defer cancel()

	publicHostName := fmt.Sprintf("%s.%s", name, "example.com")
	_, client, cancel = CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{publicHostName},
			Visibility: v1alpha1.IngressVisibilityExternalIP,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      proxyName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(proxyPort),
						},
					}},
				}},
			},
		}},
	})
	defer cancel()

	// Create a large enough population of requests that we can reasonably assess how
	// well the Ingress respected the percentage split.
	seen := make(map[string]float64, len(backends))

	const (
		// The total number of requests to make (as a float to avoid conversions in later computations).
		totalRequests = 1000.0
		// The increment to make for each request, so that the values of seen reflect the
		// percentage of the total number of requests we are making.
		increment = 100.0 / totalRequests
		// Allow the Ingress to be within 5% of the configured value.
		margin = 5.0
	)
	for i := 0.0; i < totalRequests; i++ {
		ri := RuntimeRequest(t, client, "http://"+publicHostName)
		if ri == nil {
			continue
		}
		seen[ri.Request.Headers.Get(headerName)] += increment
	}

	for name, want := range weights {
		got := seen[name]
		switch {
		case want == 0.0 && got > 0.0:
			// For 0% targets, we have tighter requirements.
			t.Errorf("Target %q received traffic, wanted none (0%% target).", name)
		case math.Abs(got-want) > margin:
			t.Errorf("Target %q received %f%%, wanted %f +/- %f", name, got, want, margin)
		}
	}
}

func TestVisibilityPath(t *testing.T) {
	t.Parallel()
	clients := test.Setup(t)

	// For /foo
	fooName, fooPort, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
	defer cancel()

	// For /bar
	barName, barPort, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
	defer cancel()

	// For /baz
	bazName, bazPort, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
	defer cancel()

	mainName, port, cancel := CreateRuntimeService(t, clients, networking.ServicePortNameHTTP1)
	defer cancel()

	// Use a post-split injected header to establish which split we are sending traffic to.
	const headerName = "Which-Backend"

	name := test.ObjectNameForTest(t)
	privateHostName := fmt.Sprintf("%s.%s.%s", name, test.ServingNamespace, "svc.cluster.local")
	localIngress, client, cancel := CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{privateHostName},
			Visibility: v1alpha1.IngressVisibilityClusterLocal,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Path: "/foo",
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      fooName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(fooPort),
						},
						// Append different headers to each split, which lets us identify
						// which backend we hit.
						AppendHeaders: map[string]string{
							headerName: fooName,
						},
						Percent: 100,
					}},
				}, {
					Path: "/bar",
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      barName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(barPort),
						},
						// Append different headers to each split, which lets us identify
						// which backend we hit.
						AppendHeaders: map[string]string{
							headerName: barName,
						},
						Percent: 100,
					}},
				}, {
					Path: "/baz",
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      bazName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(bazPort),
						},
						// Append different headers to each split, which lets us identify
						// which backend we hit.
						AppendHeaders: map[string]string{
							headerName: bazName,
						},
						Percent: 100,
					}},
				}, {
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      mainName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(port),
						},
						// Append different headers to each split, which lets us identify
						// which backend we hit.
						AppendHeaders: map[string]string{
							headerName: mainName,
						},
						Percent: 100,
					}},
				}},
			},
		}},
	})
	defer cancel()

	// Ensure we can't connect to the private resources
	for _, path := range []string{"", "/foo", "/bar", "/baz"} {
		RuntimeRequestWithExpectations(t, client, "http://"+privateHostName+path, []ResponseExpectation{StatusCodeExpectation(sets.NewInt(http.StatusNotFound))}, true)
	}

	loadbalancerAddress := localIngress.Status.PrivateLoadBalancer.Ingress[0].DomainInternal
	proxyName, proxyPort, cancel := CreateProxyService(t, clients, privateHostName, loadbalancerAddress)
	defer cancel()

	publicHostName := fmt.Sprintf("%s.%s", name, "example.com")
	_, client, cancel = CreateIngressReady(t, clients, v1alpha1.IngressSpec{
		Rules: []v1alpha1.IngressRule{{
			Hosts:      []string{publicHostName},
			Visibility: v1alpha1.IngressVisibilityExternalIP,
			HTTP: &v1alpha1.HTTPIngressRuleValue{
				Paths: []v1alpha1.HTTPIngressPath{{
					Splits: []v1alpha1.IngressBackendSplit{{
						IngressBackend: v1alpha1.IngressBackend{
							ServiceName:      proxyName,
							ServiceNamespace: test.ServingNamespace,
							ServicePort:      intstr.FromInt(proxyPort),
						},
					}},
				}},
			},
		}},
	})
	defer cancel()

	tests := map[string]string{
		"/foo":  fooName,
		"/bar":  barName,
		"/baz":  bazName,
		"":      mainName,
		"/asdf": mainName,
	}

	for path, want := range tests {
		t.Run(path, func(t *testing.T) {
			ri := RuntimeRequest(t, client, "http://"+publicHostName+path)
			if ri == nil {
				return
			}

			got := ri.Request.Headers.Get(headerName)
			if got != want {
				t.Errorf("Header[%q] = %q, wanted %q", headerName, got, want)
			}
		})
	}
}
<|endoftext|>


<commit_before>package operators

import (
	"context"
	"strings"

	g "github.com/onsi/ginkgo"
	o "github.com/onsi/gomega"
	s "github.com/onsi/gomega/gstruct"
	t "github.com/onsi/gomega/types"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/kube-openapi/pkg/util/sets"
	e2e "k8s.io/kubernetes/test/e2e/framework"

	config "github.com/openshift/api/config/v1"
	configclient "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"

	exutil "github.com/openshift/origin/test/extended/util"
)

var _ = g.Describe("[sig-arch] ClusterOperators", func() {
	defer g.GinkgoRecover()

	var clusterOperators []config.ClusterOperator
	whitelistNoNamespace := sets.NewString(
		"cloud-credential",
		"image-registry",
		"machine-api",
		"marketplace",
		"network",
		"operator-lifecycle-manager",
		"operator-lifecycle-manager-catalog",
		"support",
	)
	whitelistNoOperatorConfig := sets.NewString(
		"cloud-credential",
		"cluster-autoscaler",
		"machine-api",
		"machine-config",
		"marketplace",
		"network",
		"operator-lifecycle-manager",
		"operator-lifecycle-manager-catalog",
		"support",
	)

	g.BeforeEach(func() {
		kubeConfig, err := e2e.LoadConfig()
		o.Expect(err).ToNot(o.HaveOccurred())
		configClient, err := configclient.NewForConfig(kubeConfig)
		o.Expect(err).ToNot(o.HaveOccurred())
		clusterOperatorsList, err := configClient.ClusterOperators().List(context.Background(), metav1.ListOptions{})
		o.Expect(err).ToNot(o.HaveOccurred())
		clusterOperators = clusterOperatorsList.Items
	})

	g.Context("should define", func() {
		g.Specify("at least one namespace in their lists of related objects", func() {
			for _, clusterOperator := range clusterOperators {
				if !whitelistNoNamespace.Has(clusterOperator.Name) {
					o.Expect(clusterOperator.Status.RelatedObjects).To(o.ContainElement(isNamespace()), "ClusterOperator: %s", clusterOperator.Name)
				}
			}

		})

		oc := exutil.NewCLI("clusteroperators")
		g.Specify("at least one related object that is not a namespace", func() {
			controlplaneTopology, err := exutil.GetControlPlaneTopology(oc)
			o.Expect(err).NotTo(o.HaveOccurred())

			if *controlplaneTopology == config.ExternalTopologyMode {
				// The packageserver runs in a different cluster along the other controlplane components
				// when the controlplane is external.
				whitelistNoOperatorConfig.Insert("operator-lifecycle-manager-packageserver")
			}
			for _, clusterOperator := range clusterOperators {
				if !whitelistNoOperatorConfig.Has(clusterOperator.Name) {
					o.Expect(clusterOperator.Status.RelatedObjects).To(o.ContainElement(o.Not(isNamespace())), "ClusterOperator: %s", clusterOperator.Name)
				}
			}
		})

		g.Specify("valid related objects", func() {
			controlplaneTopology, err := exutil.GetControlPlaneTopology(oc)
			o.Expect(err).NotTo(o.HaveOccurred())
			if *controlplaneTopology == config.ExternalTopologyMode {
				// The packageserver runs in a different cluster along the other controlplane components
				// when the controlplane is external.
				whitelistNoOperatorConfig.Insert("operator-lifecycle-manager-packageserver")
			}
			restMapper := oc.RESTMapper()

			for _, clusterOperator := range clusterOperators {
				if !whitelistNoOperatorConfig.Has(clusterOperator.Name) {
					for _, relatedObj := range clusterOperator.Status.RelatedObjects {

						// any uppercase immediately signals an improper mapping:
						o.Expect(relatedObj.Resource).To(o.Equal(strings.ToLower(relatedObj.Resource)),
							"Cluster operator %s should be using lowercase resource references: %s",
							clusterOperator.Name, relatedObj.Resource)

						// also ensure we find a valid rest mapping:
						resourceMatches, err := restMapper.ResourcesFor(schema.GroupVersionResource{
							Group:    relatedObj.Group,
							Resource: relatedObj.Resource,
						})
						o.Expect(err).ToNot(o.HaveOccurred())
						o.Expect(len(resourceMatches)).To(o.BeNumerically(">", 0),
							"No valid rest mapping found for cluster operator %s related object %s",
							clusterOperator.Name, relatedObj.Resource)
					}
				}
			}
		})

	})
})

func isNamespace() t.GomegaMatcher {
	return s.MatchFields(s.IgnoreExtras|s.IgnoreMissing, s.Fields{
		"Resource": o.Equal("namespaces"),
		"Group":    o.Equal(""),
	})
}
<commit_msg>Skip known invalid relatedResource on storage ClusterOperator.<commit_after>package operators

import (
	"context"
	"strings"

	g "github.com/onsi/ginkgo"
	o "github.com/onsi/gomega"
	s "github.com/onsi/gomega/gstruct"
	t "github.com/onsi/gomega/types"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/kube-openapi/pkg/util/sets"
	e2e "k8s.io/kubernetes/test/e2e/framework"

	config "github.com/openshift/api/config/v1"
	configclient "github.com/openshift/client-go/config/clientset/versioned/typed/config/v1"

	exutil "github.com/openshift/origin/test/extended/util"
)

var _ = g.Describe("[sig-arch] ClusterOperators", func() {
	defer g.GinkgoRecover()

	var clusterOperators []config.ClusterOperator
	whitelistNoNamespace := sets.NewString(
		"cloud-credential",
		"image-registry",
		"machine-api",
		"marketplace",
		"network",
		"operator-lifecycle-manager",
		"operator-lifecycle-manager-catalog",
		"support",
	)
	whitelistNoOperatorConfig := sets.NewString(
		"cloud-credential",
		"cluster-autoscaler",
		"machine-api",
		"machine-config",
		"marketplace",
		"network",
		"operator-lifecycle-manager",
		"operator-lifecycle-manager-catalog",
		"support",
	)

	g.BeforeEach(func() {
		kubeConfig, err := e2e.LoadConfig()
		o.Expect(err).ToNot(o.HaveOccurred())
		configClient, err := configclient.NewForConfig(kubeConfig)
		o.Expect(err).ToNot(o.HaveOccurred())
		clusterOperatorsList, err := configClient.ClusterOperators().List(context.Background(), metav1.ListOptions{})
		o.Expect(err).ToNot(o.HaveOccurred())
		clusterOperators = clusterOperatorsList.Items
	})

	g.Context("should define", func() {
		g.Specify("at least one namespace in their lists of related objects", func() {
			for _, clusterOperator := range clusterOperators {
				if !whitelistNoNamespace.Has(clusterOperator.Name) {
					o.Expect(clusterOperator.Status.RelatedObjects).To(o.ContainElement(isNamespace()), "ClusterOperator: %s", clusterOperator.Name)
				}
			}

		})

		oc := exutil.NewCLI("clusteroperators")
		g.Specify("at least one related object that is not a namespace", func() {
			controlplaneTopology, err := exutil.GetControlPlaneTopology(oc)
			o.Expect(err).NotTo(o.HaveOccurred())

			if *controlplaneTopology == config.ExternalTopologyMode {
				// The packageserver runs in a different cluster along the other controlplane components
				// when the controlplane is external.
				whitelistNoOperatorConfig.Insert("operator-lifecycle-manager-packageserver")
			}
			for _, clusterOperator := range clusterOperators {
				if !whitelistNoOperatorConfig.Has(clusterOperator.Name) {
					o.Expect(clusterOperator.Status.RelatedObjects).To(o.ContainElement(o.Not(isNamespace())), "ClusterOperator: %s", clusterOperator.Name)
				}
			}
		})

		g.Specify("valid related objects", func() {
			controlplaneTopology, err := exutil.GetControlPlaneTopology(oc)
			o.Expect(err).NotTo(o.HaveOccurred())
			if *controlplaneTopology == config.ExternalTopologyMode {
				// The packageserver runs in a different cluster along the other controlplane components
				// when the controlplane is external.
				whitelistNoOperatorConfig.Insert("operator-lifecycle-manager-packageserver")
			}
			restMapper := oc.RESTMapper()

			for _, clusterOperator := range clusterOperators {
				if !whitelistNoOperatorConfig.Has(clusterOperator.Name) {
					for _, relatedObj := range clusterOperator.Status.RelatedObjects {

						// any uppercase immediately signals an improper mapping:
						o.Expect(relatedObj.Resource).To(o.Equal(strings.ToLower(relatedObj.Resource)),
							"Cluster operator %s should be using lowercase resource references: %s",
							clusterOperator.Name, relatedObj.Resource)

						// also ensure we find a valid rest mapping:

						// "storage" ClusterOperator has some relatedResource refs to CRDs that only exists if
						// TechPreviewNoUpgrade is enabled. This is acceptable and not a bug, but needs a special case here.
						if clusterOperator.Name == "storage" && (relatedObj.Resource == "sharedconfigmaps" ||
							relatedObj.Resource == "sharedsecrets") {
							continue
						}

						resourceMatches, err := restMapper.ResourcesFor(schema.GroupVersionResource{
							Group:    relatedObj.Group,
							Resource: relatedObj.Resource,
						})
						o.Expect(err).ToNot(o.HaveOccurred())
						o.Expect(len(resourceMatches)).To(o.BeNumerically(">", 0),
							"No valid rest mapping found for cluster operator %s related object %s",
							clusterOperator.Name, relatedObj.Resource)
					}
				}
			}
		})

	})
})

func isNamespace() t.GomegaMatcher {
	return s.MatchFields(s.IgnoreExtras|s.IgnoreMissing, s.Fields{
		"Resource": o.Equal("namespaces"),
		"Group":    o.Equal(""),
	})
}
<|endoftext|>


<commit_before>package network

import (
	"log"
	"os"
)

// ResetHostname changes the hostname for the local machine
func ResetHostname(newname string) error {
	//	Get hostname:
	name, err := os.Hostname()
	if err != nil {
		log.Printf("[ERROR] Problem getting hostname: %v", err.Error())
	}

	log.Printf("[INFO] Current hostname: %v -- desired new hostname: %v\n", name, newname)

	return nil
}
<commit_msg>Add code to write the hostname changes to the appropriate files<commit_after>package network

import (
	"io/ioutil"
	"log"
	"os"
	"strings"
)

// ResetHostname changes the hostname for the local machine
func ResetHostname(newname string) error {
	//	Get hostname:
	name, err := os.Hostname()
	if err != nil {
		log.Printf("[ERROR] Problem getting hostname: %v", err.Error())
		return err
	}

	log.Printf("[INFO] Current hostname: %v -- desired new hostname: %v\n", name, newname)

	//	Update /etc/hostname file
	hostname, err := ioutil.ReadFile("/etc/hostname")
	if err != nil {
		log.Printf("[ERROR] Problem reading /etc/hostname: %v", err.Error())
		return err
	}
	newhostname := strings.Replace(string(hostname[:]), "raspberrypi", newname, -1)
	err = ioutil.WriteFile("/etc/hostname", newhostname, 0644)
	if err != nil {
		log.Printf("[ERROR] Problem writing /etc/hostname: %v", err.Error())
		return err
	}

	//	Update the /etc/hosts file
	hosts, err := ioutil.ReadFile("/etc/hosts")
	if err != nil {
		log.Printf("[ERROR] Problem reading /etc/hosts: %v", err.Error())
		return err
	}
	newhosts := strings.Replace(string(hosts[:]), "raspberrypi", newname, -1)
	err = ioutil.WriteFile("/etc/hosts", newhosts, 0644)
	if err != nil {
		log.Printf("[ERROR] Problem writing /etc/hosts: %v", err.Error())
		return err
	}

	return nil
}
<|endoftext|>


<commit_before>// +build integration
// To turn on this test use -tags=integration in go test command

package credentials

import (
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/jcmturner/gokrb5.v4/testdata"
)

const (
	kinitCmd = "kinit"
	kvnoCmd  = "kvno"
)

func login() error {
	file, err := os.Create("/etc/krb5.conf")
	if err != nil {
		return fmt.Errorf("cannot open krb5.conf: %v", err)
	}
	defer file.Close()
	fmt.Fprintf(file, testdata.TEST_KRB5CONF)

	cmd := exec.Command(kinitCmd, "testuser1@TEST.GOKRB5")
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return fmt.Errorf("could not open stdin to %s command: %v", kinitCmd, err)
	}
	err = cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kinitCmd, err)
	}
	go func() {
		defer stdin.Close()
		io.WriteString(stdin, "passwordvalue")
	}()
	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v", kinitCmd, err)
	}
	return nil
}

func getServiceTkt() error {
	cmd := exec.Command(kvnoCmd, "HTTP/host.test.gokrb5")
	err := cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kvnoCmd, err)
	}
	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v", kvnoCmd, err)
	}
	return nil
}

func TestLoadCCache(t *testing.T) {
	err := login()
	if err != nil {
		t.Fatalf("error logging in with kinit: %v", err)
	}
	usr, _ := user.Current()
	cpath := "/tmp/krb5cc_" + usr.Uid
	c, err := LoadCCache(cpath)
	pn := c.GetClientPrincipalName()
	assert.Equal(t, "testuser1@TEST.GOKRB5", pn.GetPrincipalNameString(), "principal not as expected")
}
<commit_msg>fix ccache tests<commit_after>// +build integration
// To turn on this test use -tags=integration in go test command

package credentials

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"os/exec"
	"os/user"
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/jcmturner/gokrb5.v4/testdata"
)

const (
	kinitCmd = "kinit"
	kvnoCmd  = "kvno"
)

func login() error {
	file, err := os.Create("/etc/krb5.conf")
	if err != nil {
		return fmt.Errorf("cannot open krb5.conf: %v", err)
	}
	defer file.Close()
	fmt.Fprintf(file, testdata.TEST_KRB5CONF)

	cmd := exec.Command(kinitCmd, "testuser1@TEST.GOKRB5")
	stdin, err := cmd.StdinPipe()
	if err != nil {
		return fmt.Errorf("could not open stdin to %s command: %v", kinitCmd, err)
	}
	err = cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kinitCmd, err)
	}
	go func() {
		defer stdin.Close()
		io.WriteString(stdin, "passwordvalue")
	}()
	stderr, _ := cmd.StderrPipe()
	errBuf := new(bytes.Buffer)
	go func() {
		defer stderr.Close()
		io.Copy(errBuf, stderr)
	}()
	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v stderr: %s", kinitCmd, err, string(errBuf.Bytes()))
	}
	return nil
}

func getServiceTkt() error {
	cmd := exec.Command(kvnoCmd, "HTTP/host.test.gokrb5")
	err := cmd.Start()
	if err != nil {
		return fmt.Errorf("could not start %s command: %v", kvnoCmd, err)
	}
	err = cmd.Wait()
	if err != nil {
		return fmt.Errorf("%s did not run successfully: %v", kvnoCmd, err)
	}
	return nil
}

func TestLoadCCache(t *testing.T) {
	err := login()
	if err != nil {
		t.Fatalf("error logging in with kinit: %v", err)
	}
	usr, _ := user.Current()
	cpath := "/tmp/krb5cc_" + usr.Uid
	c, err := LoadCCache(cpath)
	pn := c.GetClientPrincipalName()
	assert.Equal(t, "testuser1@TEST.GOKRB5", pn.GetPrincipalNameString(), "principal not as expected")
}
<|endoftext|>


<commit_before>// +build integrationtest

package integrationtest

import (
	"testing"
	"time"

	storkv1 "github.com/libopenstorage/stork/pkg/apis/stork/v1alpha1"
	storkops "github.com/portworx/sched-ops/k8s/stork"
	"github.com/portworx/torpedo/drivers/scheduler"
	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/require"
)

func TestMigrationBackup(t *testing.T) {
	// reset mock time before running any tests
	err := setMockTime(nil)
	require.NoError(t, err, "Error resetting mock time")

	setDefaultsForBackup(t)

	logrus.Infof("Using stork volume driver: %s", volumeDriverName)
	logrus.Infof("Backup path being used: %s", backupLocationPath)

	t.Run("deploymentMigrationBackupTest", deploymentMigrationBackupTest)
}

func deploymentMigrationBackupTest(t *testing.T) {
	for location, secret := range allConfigMap {
		logrus.Infof("Backing up to cloud: %v using secret %v", location, secret)
		var err error
		// Reset config in case of error
		defer func() {
			err = setRemoteConfig("")
			require.NoError(t, err, "Error resetting remote config")
		}()

		// Trigger migration to backup app from source cluster to destination cluster
		ctxs, preMigrationCtx := triggerMigration(
			t,
			"mysql-migration",
			"mysql-1-pvc",
			nil,
			[]string{"mysql-migration"},
			true,
			false,
			true,
		)

		// Cleanup up source
		validateAndDestroyMigration(t, ctxs, preMigrationCtx, true, true, true, false, true)

		logrus.Infoln("Completed migration of apps from source to destination, will now start backup on second cluster")

		// Change kubeconfig to destination cluster
		err = dumpRemoteKubeConfig(remoteConfig)
		require.NoErrorf(t, err, "Unable to write clusterconfig: %v", err)

		err = setRemoteConfig(remoteFilePath)
		require.NoError(t, err, "Error setting remote config")

		// Backup app from the destination cluster to cloud
		currBackupLocation, err := createBackupLocation(t, appKey+"-backup-location", testKey+"-mysql-migration", storkv1.BackupLocationType(location), secret)
		require.NoError(t, err, "Error creating backuplocation %s", currBackupLocation.Name)

		currBackup, err := createApplicationBackupWithAnnotation(t, "dest-backup", testKey+"-mysql-migration", currBackupLocation)
		require.NoError(t, err, "Error creating app backups")

		err = waitForAppBackupCompletion(currBackup.Name, currBackup.Namespace, applicationBackupSyncRetryTimeout)
		require.NoError(t, err, "Application backup %s in namespace %s failed.", currBackup.Name, currBackup.Namespace)

		destroyAndWait(t, []*scheduler.Context{preMigrationCtx})

		logrus.Infoln("Completed backup of apps destination cluster")

		// Switch kubeconfig to source cluster for restore
		err = setRemoteConfig("")
		require.NoError(t, err, "Error resetting remote config")

		srcBackupLocation, err := createBackupLocation(t, appKey+"-backup-location", testKey+"-mysql-migration", storkv1.BackupLocationType(location), secret)
		require.NoError(t, err, "Error creating backuplocation %s", currBackupLocation.Name)

		// Set sync to true on first cluster so that backup from second cluster is available
		srcBackupLocation.Location.Sync = true
		srcBackupLocation, err = storkops.Instance().UpdateBackupLocation(srcBackupLocation)
		require.NoError(t, err, "Failed to set backup-location sync to true")

		backupToRestore, err := getSyncedBackupWithAnnotation(currBackup, backupSyncAnnotation)
		require.NotNil(t, backupToRestore, "Backup sync failed. Backup not found on the second cluster")

		appRestoreForBackup, err := createApplicationRestore(t, "restore-from-dest-backup", srcBackupLocation.Namespace, backupToRestore, srcBackupLocation)
		require.Nil(t, err, "failure to create restore object in namespace %s", currBackup.Namespace)
		require.NotNil(t, appRestoreForBackup, "failure to restore backup in namespace %s", currBackup.Namespace)

		err = schedulerDriver.WaitForRunning(preMigrationCtx, defaultWaitTimeout, defaultWaitInterval)
		require.NoError(t, err, "Error waiting for app on source cluster post restore from dest backup")

		logrus.Infoln("Completed restore of apps on source cluster")

		// Set sync to false on first cluster so that no more backups are sync'ed
		srcBackupLocation.Location.Sync = false
		srcBackupLocation, err = storkops.Instance().UpdateBackupLocation(srcBackupLocation)
		require.NoError(t, err, "Failed to set backup-location sync to true")

		time.Sleep(time.Second * 30)

		err = deleteAndWaitForBackupDeletion(srcBackupLocation.Namespace)
		require.NoError(t, err, "All backups not deleted on the source cluster post migration-backup-restore: %v.", err)

		time.Sleep(time.Second * 30)

		err = storkops.Instance().DeleteBackupLocation(srcBackupLocation.Name, srcBackupLocation.Namespace)
		require.NoError(t, err, "Failed to delete  backup location %s on source cluster: %v.", srcBackupLocation.Name, err)

		destroyAndWait(t, []*scheduler.Context{preMigrationCtx})

		err = deleteApplicationRestoreList([]*storkv1.ApplicationRestore{appRestoreForBackup})
		require.NoError(t, err, "failure to delete application restore %s: %v", appRestoreForBackup.Name, err)

		logrus.Infoln("Completed cleanup on source cluster")

		// Change kubeconfig to second cluster
		err = dumpRemoteKubeConfig(remoteConfig)
		require.NoErrorf(t, err, "Unable to write clusterconfig: %v", err)

		err = setRemoteConfig(remoteFilePath)
		require.NoError(t, err, "Error setting remote config")
		err = deleteAndWaitForBackupDeletion(currBackup.Namespace)
		require.NoError(t, err, "All backups not deleted on the source cluster post migration-backup-restore: %v.", err)

		err = storkops.Instance().DeleteBackupLocation(currBackupLocation.Name, currBackupLocation.Namespace)
		require.NoError(t, err, "Failed to delete  backup location %s on source cluster: %v.", currBackupLocation.Name, err)

		logrus.Infoln("Completed cleanup on destination cluster")
	}
}
<commit_msg>Explicitly reset kubeconfig after every migration-backup loop<commit_after>// +build integrationtest

package integrationtest

import (
	"testing"
	"time"

	storkv1 "github.com/libopenstorage/stork/pkg/apis/stork/v1alpha1"
	storkops "github.com/portworx/sched-ops/k8s/stork"
	"github.com/portworx/torpedo/drivers/scheduler"
	"github.com/sirupsen/logrus"
	"github.com/stretchr/testify/require"
)

func TestMigrationBackup(t *testing.T) {
	// reset mock time before running any tests
	err := setMockTime(nil)
	require.NoError(t, err, "Error resetting mock time")

	setDefaultsForBackup(t)

	logrus.Infof("Using stork volume driver: %s", volumeDriverName)
	logrus.Infof("Backup path being used: %s", backupLocationPath)

	t.Run("deploymentMigrationBackupTest", deploymentMigrationBackupTest)
}

func deploymentMigrationBackupTest(t *testing.T) {
	for location, secret := range allConfigMap {
		logrus.Infof("Backing up to cloud: %v using secret %v", location, secret)
		var err error
		// Reset config in case of error
		defer func() {
			err = setRemoteConfig("")
			require.NoError(t, err, "Error resetting remote config")
		}()

		// Trigger migration to backup app from source cluster to destination cluster
		ctxs, preMigrationCtx := triggerMigration(
			t,
			"mysql-migration",
			"mysql-1-pvc",
			nil,
			[]string{"mysql-migration"},
			true,
			false,
			true,
		)

		// Cleanup up source
		validateAndDestroyMigration(t, ctxs, preMigrationCtx, true, true, true, false, true)

		logrus.Infoln("Completed migration of apps from source to destination, will now start backup on second cluster")

		// Change kubeconfig to destination cluster
		err = dumpRemoteKubeConfig(remoteConfig)
		require.NoErrorf(t, err, "Unable to write clusterconfig: %v", err)

		err = setRemoteConfig(remoteFilePath)
		require.NoError(t, err, "Error setting remote config")

		// Backup app from the destination cluster to cloud
		currBackupLocation, err := createBackupLocation(t, appKey+"-backup-location", testKey+"-mysql-migration", storkv1.BackupLocationType(location), secret)
		require.NoError(t, err, "Error creating backuplocation %s", currBackupLocation.Name)

		currBackup, err := createApplicationBackupWithAnnotation(t, "dest-backup", testKey+"-mysql-migration", currBackupLocation)
		require.NoError(t, err, "Error creating app backups")

		err = waitForAppBackupCompletion(currBackup.Name, currBackup.Namespace, applicationBackupSyncRetryTimeout)
		require.NoError(t, err, "Application backup %s in namespace %s failed.", currBackup.Name, currBackup.Namespace)

		destroyAndWait(t, []*scheduler.Context{preMigrationCtx})

		logrus.Infoln("Completed backup of apps destination cluster")

		// Switch kubeconfig to source cluster for restore
		err = setRemoteConfig("")
		require.NoError(t, err, "Error resetting remote config")

		srcBackupLocation, err := createBackupLocation(t, appKey+"-backup-location", testKey+"-mysql-migration", storkv1.BackupLocationType(location), secret)
		require.NoError(t, err, "Error creating backuplocation %s", currBackupLocation.Name)

		// Set sync to true on first cluster so that backup from second cluster is available
		srcBackupLocation.Location.Sync = true
		srcBackupLocation, err = storkops.Instance().UpdateBackupLocation(srcBackupLocation)
		require.NoError(t, err, "Failed to set backup-location sync to true")

		backupToRestore, err := getSyncedBackupWithAnnotation(currBackup, backupSyncAnnotation)
		require.NotNil(t, backupToRestore, "Backup sync failed. Backup not found on the second cluster")

		appRestoreForBackup, err := createApplicationRestore(t, "restore-from-dest-backup", srcBackupLocation.Namespace, backupToRestore, srcBackupLocation)
		require.Nil(t, err, "failure to create restore object in namespace %s", currBackup.Namespace)
		require.NotNil(t, appRestoreForBackup, "failure to restore backup in namespace %s", currBackup.Namespace)

		err = schedulerDriver.WaitForRunning(preMigrationCtx, defaultWaitTimeout, defaultWaitInterval)
		require.NoError(t, err, "Error waiting for app on source cluster post restore from dest backup")

		logrus.Infoln("Completed restore of apps on source cluster")

		// Set sync to false on first cluster so that no more backups are sync'ed
		srcBackupLocation.Location.Sync = false
		srcBackupLocation, err = storkops.Instance().UpdateBackupLocation(srcBackupLocation)
		require.NoError(t, err, "Failed to set backup-location sync to true")

		time.Sleep(time.Second * 30)

		err = deleteAndWaitForBackupDeletion(srcBackupLocation.Namespace)
		require.NoError(t, err, "All backups not deleted on the source cluster post migration-backup-restore: %v.", err)

		time.Sleep(time.Second * 30)

		err = storkops.Instance().DeleteBackupLocation(srcBackupLocation.Name, srcBackupLocation.Namespace)
		require.NoError(t, err, "Failed to delete  backup location %s on source cluster: %v.", srcBackupLocation.Name, err)

		destroyAndWait(t, []*scheduler.Context{preMigrationCtx})

		err = deleteApplicationRestoreList([]*storkv1.ApplicationRestore{appRestoreForBackup})
		require.NoError(t, err, "failure to delete application restore %s: %v", appRestoreForBackup.Name, err)

		logrus.Infoln("Completed cleanup on source cluster")

		// Change kubeconfig to second cluster
		err = dumpRemoteKubeConfig(remoteConfig)
		require.NoErrorf(t, err, "Unable to write clusterconfig: %v", err)

		err = setRemoteConfig(remoteFilePath)
		require.NoError(t, err, "Error setting remote config")
		err = deleteAndWaitForBackupDeletion(currBackup.Namespace)
		require.NoError(t, err, "All backups not deleted on the source cluster post migration-backup-restore: %v.", err)

		err = storkops.Instance().DeleteBackupLocation(currBackupLocation.Name, currBackupLocation.Namespace)
		require.NoError(t, err, "Failed to delete  backup location %s on source cluster: %v.", currBackupLocation.Name, err)

		logrus.Infoln("Completed cleanup on destination cluster")

		err = setRemoteConfig("")
		require.NoError(t, err, "Error resetting remote config")
	}
}
<|endoftext|>


<commit_before>package test

import (
	"fmt"
	"sort"
	"testing"
	"time"

	"github.com/prometheus/common/model"
)

// Collector gathers alerts received by a notification destination
// and verifies whether all arrived and within the correct time boundaries.
type Collector struct {
	t    *testing.T
	name string
	opts *AcceptanceOpts

	collected map[float64][]model.Alerts
	expected  map[Interval][]model.Alerts
}

func (c *Collector) String() string {
	return c.name
}

func batchesEqual(as, bs model.Alerts, opts *AcceptanceOpts) bool {
	if len(as) != len(bs) {
		return false
	}

	// Ensure sorting.
	sort.Sort(as)
	sort.Sort(bs)

	for i, a := range as {
		if !equalAlerts(a, bs[i], opts) {
			fmt.Println(a, bs[i])
			fmt.Printf("%s != %s\n", a.StartsAt, bs[i].StartsAt)
			return false
		}
	}
	return true
}

// latest returns the latest relative point in time where a notification is
// expected.
func (c *Collector) latest() float64 {
	var latest float64
	for iv := range c.expected {
		if iv.end > latest {
			latest = iv.end
		}
	}
	return latest
}

// want declares that the Collector expects to receive the given alerts
// within the given time boundaries.
func (c *Collector) Want(iv Interval, alerts ...*TestAlert) {
	var nas model.Alerts
	for _, a := range alerts {
		nas = append(nas, a.nativeAlert(c.opts))
	}

	c.expected[iv] = append(c.expected[iv], nas)
}

// add the given alerts to the collected alerts.
func (c *Collector) add(alerts ...*model.Alert) {
	arrival := c.opts.relativeTime(time.Now())

	c.collected[arrival] = append(c.collected[arrival], model.Alerts(alerts))
}

func (c *Collector) check() string {
	report := fmt.Sprintf("\ncollector %q:\n\n", c)

	for iv, expected := range c.expected {
		report += fmt.Sprintf("interval %v\n", iv)

		for _, exp := range expected {
			var found model.Alerts

			report += fmt.Sprintf("---\n")

			for _, e := range exp {
				report += fmt.Sprintf("- %v\n", e)
			}

			for at, got := range c.collected {
				if !iv.contains(at) {
					continue
				}
				for _, a := range got {
					if batchesEqual(exp, a, c.opts) {
						found = a
						break
					}
				}
				if found != nil {
					break
				}
			}

			if found != nil {
				report += fmt.Sprintf("  [ ✓ ]\n")
			} else {
				c.t.Fail()
				report += fmt.Sprintf("  [ ✗ ]\n")
			}
		}
	}

	// Detect unexpected notifications.
	var totalExp, totalAct int
	for _, exp := range c.expected {
		for _, e := range exp {
			totalExp += len(e)
		}
	}
	for _, act := range c.collected {
		for _, a := range act {
			if len(a) == 0 {
				c.t.Error("received empty notifications")
			}
			totalAct += len(a)
		}
	}
	if totalExp != totalAct {
		c.t.Fail()
		report += fmt.Sprintf("\nExpected total of %d alerts, got %d", totalExp, totalAct)
	}

	if c.t.Failed() {
		report += "\nreceived:\n"

		for at, col := range c.collected {
			for _, alerts := range col {
				report += fmt.Sprintf("@ %v\n", at)
				for _, a := range alerts {
					report += fmt.Sprintf("- %v\n", a)
				}
			}
		}
	}

	return report
}
<commit_msg>Remove some debug statements.<commit_after>package test

import (
	"fmt"
	"sort"
	"testing"
	"time"

	"github.com/prometheus/common/model"
)

// Collector gathers alerts received by a notification destination
// and verifies whether all arrived and within the correct time boundaries.
type Collector struct {
	t    *testing.T
	name string
	opts *AcceptanceOpts

	collected map[float64][]model.Alerts
	expected  map[Interval][]model.Alerts
}

func (c *Collector) String() string {
	return c.name
}

func batchesEqual(as, bs model.Alerts, opts *AcceptanceOpts) bool {
	if len(as) != len(bs) {
		return false
	}

	// Ensure sorting.
	sort.Sort(as)
	sort.Sort(bs)

	for i, a := range as {
		if !equalAlerts(a, bs[i], opts) {
			return false
		}
	}
	return true
}

// latest returns the latest relative point in time where a notification is
// expected.
func (c *Collector) latest() float64 {
	var latest float64
	for iv := range c.expected {
		if iv.end > latest {
			latest = iv.end
		}
	}
	return latest
}

// want declares that the Collector expects to receive the given alerts
// within the given time boundaries.
func (c *Collector) Want(iv Interval, alerts ...*TestAlert) {
	var nas model.Alerts
	for _, a := range alerts {
		nas = append(nas, a.nativeAlert(c.opts))
	}

	c.expected[iv] = append(c.expected[iv], nas)
}

// add the given alerts to the collected alerts.
func (c *Collector) add(alerts ...*model.Alert) {
	arrival := c.opts.relativeTime(time.Now())

	c.collected[arrival] = append(c.collected[arrival], model.Alerts(alerts))
}

func (c *Collector) check() string {
	report := fmt.Sprintf("\ncollector %q:\n\n", c)

	for iv, expected := range c.expected {
		report += fmt.Sprintf("interval %v\n", iv)

		for _, exp := range expected {
			var found model.Alerts

			report += fmt.Sprintf("---\n")

			for _, e := range exp {
				report += fmt.Sprintf("- %v\n", e)
			}

			for at, got := range c.collected {
				if !iv.contains(at) {
					continue
				}
				for _, a := range got {
					if batchesEqual(exp, a, c.opts) {
						found = a
						break
					}
				}
				if found != nil {
					break
				}
			}

			if found != nil {
				report += fmt.Sprintf("  [ ✓ ]\n")
			} else {
				c.t.Fail()
				report += fmt.Sprintf("  [ ✗ ]\n")
			}
		}
	}

	// Detect unexpected notifications.
	var totalExp, totalAct int
	for _, exp := range c.expected {
		for _, e := range exp {
			totalExp += len(e)
		}
	}
	for _, act := range c.collected {
		for _, a := range act {
			if len(a) == 0 {
				c.t.Error("received empty notifications")
			}
			totalAct += len(a)
		}
	}
	if totalExp != totalAct {
		c.t.Fail()
		report += fmt.Sprintf("\nExpected total of %d alerts, got %d", totalExp, totalAct)
	}

	if c.t.Failed() {
		report += "\nreceived:\n"

		for at, col := range c.collected {
			for _, alerts := range col {
				report += fmt.Sprintf("@ %v\n", at)
				for _, a := range alerts {
					report += fmt.Sprintf("- %v\n", a)
				}
			}
		}
	}

	return report
}
<|endoftext|>


<commit_before>// +build darwin linux
// run

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Test that maps don't go quadratic for NaNs and other values.

package main

import (
	"fmt"
	"math"
	"time"
)

// checkLinear asserts that the running time of f(n) is in O(n).
// tries is the initial number of iterations.
func checkLinear(typ string, tries int, f func(n int)) {
	// Depending on the machine and OS, this test might be too fast
	// to measure with accurate enough granularity. On failure,
	// make it run longer, hoping that the timing granularity
	// is eventually sufficient.

	timeF := func(n int) time.Duration {
		t1 := time.Now()
		f(n)
		return time.Since(t1)
	}

	t0 := time.Now()

	n := tries
	fails := 0
	for {
		t1 := timeF(n)
		t2 := timeF(2 * n)

		// should be 2x (linear); allow up to 3x
		if t2 < 3*t1 {
			if false {
				fmt.Println(typ, "\t", time.Since(t0))
			}
			return
		}
		fails++
		if fails == 6 {
			panic(fmt.Sprintf("%s: too slow: %d inserts: %v; %d inserts: %v\n",
				typ, n, t1, 2*n, t2))
		}
		if fails < 4 {
			n *= 2
		}
	}
}

type I interface {
	f()
}

type C int

func (C) f() {}

func main() {
	// NaNs. ~31ms on a 1.6GHz Zeon.
	checkLinear("NaN", 30000, func(n int) {
		m := map[float64]int{}
		nan := math.NaN()
		for i := 0; i < n; i++ {
			m[nan] = 1
		}
		if len(m) != n {
			panic("wrong size map after nan insertion")
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("eface", 10000, func(n int) {
		m := map[interface{}]int{}
		for i := 0; i < n; i++ {
			m[i] = 1
		}
	})

	// ~7ms on a 1.6GHz Zeon.
	// Regression test for CL 119360043.
	checkLinear("iface", 10000, func(n int) {
		m := map[I]int{}
		for i := 0; i < n; i++ {
			m[C(i)] = 1
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("int", 10000, func(n int) {
		m := map[int]int{}
		for i := 0; i < n; i++ {
			m[i] = 1
		}
	})

	// ~18ms on a 1.6GHz Zeon.
	checkLinear("string", 10000, func(n int) {
		m := map[string]int{}
		for i := 0; i < n; i++ {
			m[fmt.Sprint(i)] = 1
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("float32", 10000, func(n int) {
		m := map[float32]int{}
		for i := 0; i < n; i++ {
			m[float32(i)] = 1
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("float64", 10000, func(n int) {
		m := map[float64]int{}
		for i := 0; i < n; i++ {
			m[float64(i)] = 1
		}
	})

	// ~22ms on a 1.6GHz Zeon.
	checkLinear("complex64", 10000, func(n int) {
		m := map[complex64]int{}
		for i := 0; i < n; i++ {
			m[complex(float32(i), float32(i))] = 1
		}
	})

	// ~32ms on a 1.6GHz Zeon.
	checkLinear("complex128", 10000, func(n int) {
		m := map[complex128]int{}
		for i := 0; i < n; i++ {
			m[complex(float64(i), float64(i))] = 1
		}
	})

	// ~70ms on a 1.6GHz Zeon.
	// The iterate/delete idiom currently takes expected
	// O(n lg n) time.  Fortunately, the checkLinear test
	// leaves enough wiggle room to include n lg n time
	// (it actually tests for O(n^log_2(3)).
	checkLinear("iterdelete", 10000, func(n int) {
		m := map[int]int{}
		for i := 0; i < n; i++ {
			m[i] = i
		}
		for i := 0; i < n; i++ {
			for k := range m {
				delete(m, k)
				break
			}
		}
	})
}
<commit_msg>test: make maplinear iterdelete test less flaky<commit_after>// +build darwin linux
// run

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Test that maps don't go quadratic for NaNs and other values.

package main

import (
	"fmt"
	"math"
	"time"
)

// checkLinear asserts that the running time of f(n) is in O(n).
// tries is the initial number of iterations.
func checkLinear(typ string, tries int, f func(n int)) {
	// Depending on the machine and OS, this test might be too fast
	// to measure with accurate enough granularity. On failure,
	// make it run longer, hoping that the timing granularity
	// is eventually sufficient.

	timeF := func(n int) time.Duration {
		t1 := time.Now()
		f(n)
		return time.Since(t1)
	}

	t0 := time.Now()

	n := tries
	fails := 0
	for {
		t1 := timeF(n)
		t2 := timeF(2 * n)

		// should be 2x (linear); allow up to 3x
		if t2 < 3*t1 {
			if false {
				fmt.Println(typ, "\t", time.Since(t0))
			}
			return
		}
		fails++
		if fails == 6 {
			panic(fmt.Sprintf("%s: too slow: %d inserts: %v; %d inserts: %v\n",
				typ, n, t1, 2*n, t2))
		}
		if fails < 4 {
			n *= 2
		}
	}
}

type I interface {
	f()
}

type C int

func (C) f() {}

func main() {
	// NaNs. ~31ms on a 1.6GHz Zeon.
	checkLinear("NaN", 30000, func(n int) {
		m := map[float64]int{}
		nan := math.NaN()
		for i := 0; i < n; i++ {
			m[nan] = 1
		}
		if len(m) != n {
			panic("wrong size map after nan insertion")
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("eface", 10000, func(n int) {
		m := map[interface{}]int{}
		for i := 0; i < n; i++ {
			m[i] = 1
		}
	})

	// ~7ms on a 1.6GHz Zeon.
	// Regression test for CL 119360043.
	checkLinear("iface", 10000, func(n int) {
		m := map[I]int{}
		for i := 0; i < n; i++ {
			m[C(i)] = 1
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("int", 10000, func(n int) {
		m := map[int]int{}
		for i := 0; i < n; i++ {
			m[i] = 1
		}
	})

	// ~18ms on a 1.6GHz Zeon.
	checkLinear("string", 10000, func(n int) {
		m := map[string]int{}
		for i := 0; i < n; i++ {
			m[fmt.Sprint(i)] = 1
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("float32", 10000, func(n int) {
		m := map[float32]int{}
		for i := 0; i < n; i++ {
			m[float32(i)] = 1
		}
	})

	// ~6ms on a 1.6GHz Zeon.
	checkLinear("float64", 10000, func(n int) {
		m := map[float64]int{}
		for i := 0; i < n; i++ {
			m[float64(i)] = 1
		}
	})

	// ~22ms on a 1.6GHz Zeon.
	checkLinear("complex64", 10000, func(n int) {
		m := map[complex64]int{}
		for i := 0; i < n; i++ {
			m[complex(float32(i), float32(i))] = 1
		}
	})

	// ~32ms on a 1.6GHz Zeon.
	checkLinear("complex128", 10000, func(n int) {
		m := map[complex128]int{}
		for i := 0; i < n; i++ {
			m[complex(float64(i), float64(i))] = 1
		}
	})

	// ~70ms on a 1.6GHz Zeon.
	// The iterate/delete idiom currently takes expected
	// O(n lg n) time.  Fortunately, the checkLinear test
	// leaves enough wiggle room to include n lg n time
	// (it actually tests for O(n^log_2(3)).
	// To prevent false positives, average away variation
	// by doing multiple rounds within a single run.
	checkLinear("iterdelete", 2500, func(n int) {
		for round := 0; round < 4; round++ {
			m := map[int]int{}
			for i := 0; i < n; i++ {
				m[i] = i
			}
			for i := 0; i < n; i++ {
				for k := range m {
					delete(m, k)
					break
				}
			}
		}
	})
}
<|endoftext|>


<commit_before>package pkg

import (
	"testing"
	"time"
)

func TestP1(t *testing.T) {
	t.Parallel()
	t.Log("t.Log(P1)")
	time.Sleep(100*time.Millisecond)
	t.Errorf("P1 error")
}

func TestP2(t *testing.T) {
	t.Parallel()
	t.Log("t.Log(P2)")
	time.Sleep(50*time.Millisecond)
	t.Errorf("P2 error")
}

func TestP3(t *testing.T) {
	t.Parallel()
	t.Log("t.Log(P3)")
	time.Sleep(75*time.Millisecond)
	t.Errorf("P3 error")
}
<commit_msg>testdata: gofmt testdata/src<commit_after>package pkg

import (
	"testing"
	"time"
)

func TestP1(t *testing.T) {
	t.Parallel()
	t.Log("t.Log(P1)")
	time.Sleep(100 * time.Millisecond)
	t.Errorf("P1 error")
}

func TestP2(t *testing.T) {
	t.Parallel()
	t.Log("t.Log(P2)")
	time.Sleep(50 * time.Millisecond)
	t.Errorf("P2 error")
}

func TestP3(t *testing.T) {
	t.Parallel()
	t.Log("t.Log(P3)")
	time.Sleep(75 * time.Millisecond)
	t.Errorf("P3 error")
}
<|endoftext|>


<commit_before>// Copyright 2018 The Prometheus 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 kubernetes

import (
	"net/url"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/metrics"
	"k8s.io/client-go/util/workqueue"
)

const (
	cacheMetricsNamespace     = metricsNamespace + "_cache"
	workqueueMetricsNamespace = metricsNamespace + "_workqueue"
)

var (
	// Metrics for client-go's HTTP requests.
	clientGoRequestResultMetricVec = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: metricsNamespace,
			Name:      "http_request_total",
			Help:      "Total number of HTTP requests to the Kubernetes API by status code.",
		},
		[]string{"status_code"},
	)
	clientGoRequestLatencyMetricVec = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace:  metricsNamespace,
			Name:       "http_request_duration_seconds",
			Help:       "Summary of latencies for HTTP requests to the Kubernetes API by endpoint.",
			Objectives: map[float64]float64{},
		},
		[]string{"endpoint"},
	)

	// Definition of metrics for client-go cache metrics provider.
	clientGoCacheListTotalMetric = prometheus.NewCounter(
		prometheus.CounterOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "list_total",
			Help:      "Total number of list operations.",
		},
	)
	clientGoCacheListDurationMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "list_duration_seconds",
			Help:       "Duration of a Kubernetes API call in seconds.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheItemsInListCountMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "list_items",
			Help:       "Count of items in a list from the Kubernetes API.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheWatchesCountMetric = prometheus.NewCounter(
		prometheus.CounterOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "watches_total",
			Help:      "Total number of watch operations.",
		},
	)
	clientGoCacheShortWatchesCountMetric = prometheus.NewCounter(
		prometheus.CounterOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "short_watches_total",
			Help:      "Total number of short watch operations.",
		},
	)
	clientGoCacheWatchesDurationMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "watch_duration_seconds",
			Help:       "Duration of watches on the Kubernetes API.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheItemsInWatchesCountMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "watch_events",
			Help:       "Number of items in watches on the Kubernetes API.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheLastResourceVersionMetric = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "last_resource_version",
			Help:      "Last resource version from the Kubernetes API.",
		},
	)

	// Definition of metrics for client-go workflow metrics provider
	clientGoWorkqueueDepthMetricVec = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "depth",
			Help:      "Current depth of the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueAddsMetricVec = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "items_total",
			Help:      "Total number of items added to the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueLatencyMetricVec = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace:  workqueueMetricsNamespace,
			Name:       "latency_seconds",
			Help:       "How long an item stays in the work queue.",
			Objectives: map[float64]float64{},
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueUnfinishedWorkSecondsMetricVec = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "unfinished_work_seconds",
			Help:      "How long an item has remained unfinished in the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueLongestRunningProcessorMetricVec = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "longest_running_processor_seconds",
			Help:      "The duration (in microseconds) of the longest running processor in the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueWorkDurationMetricVec = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace:  workqueueMetricsNamespace,
			Name:       "work_duration_seconds",
			Help:       "How long processing an item from the work queue takes.",
			Objectives: map[float64]float64{},
		},
		[]string{"queue_name"},
	)
)

// gaugeSetFunc is an adapter that allows the use of functions as gauge metric setters.
type gaugeSetFunc func(float64)

func (s gaugeSetFunc) Set(value float64) {
	s(value)
}

// Definition of dummy metric used as a placeholder if we don't want to observe some data.
type noopMetric struct{}

func (noopMetric) Inc()            {}
func (noopMetric) Dec()            {}
func (noopMetric) Observe(float64) {}

// Definition of client-go metrics adapters for HTTP requests observation
type clientGoRequestMetricAdapter struct{}

func (f *clientGoRequestMetricAdapter) Register(registerer prometheus.Registerer) {
	metrics.Register(f, f)
	registerer.MustRegister(clientGoRequestResultMetricVec)
	registerer.MustRegister(clientGoRequestLatencyMetricVec)
}
func (clientGoRequestMetricAdapter) Increment(code string, method string, host string) {
	clientGoRequestResultMetricVec.WithLabelValues(code).Inc()
}
func (clientGoRequestMetricAdapter) Observe(verb string, u url.URL, latency time.Duration) {
	clientGoRequestLatencyMetricVec.WithLabelValues(u.EscapedPath()).Observe(latency.Seconds())
}

// Definition of client-go cache metrics provider definition
type clientGoCacheMetricsProvider struct{}

func (f *clientGoCacheMetricsProvider) Register(registerer prometheus.Registerer) {
	cache.SetReflectorMetricsProvider(f)
	registerer.MustRegister(clientGoCacheWatchesDurationMetric)
	registerer.MustRegister(clientGoCacheWatchesCountMetric)
	registerer.MustRegister(clientGoCacheListDurationMetric)
	registerer.MustRegister(clientGoCacheListTotalMetric)
	registerer.MustRegister(clientGoCacheLastResourceVersionMetric)
	registerer.MustRegister(clientGoCacheShortWatchesCountMetric)
	registerer.MustRegister(clientGoCacheItemsInWatchesCountMetric)
	registerer.MustRegister(clientGoCacheItemsInListCountMetric)

}
func (clientGoCacheMetricsProvider) NewListsMetric(name string) cache.CounterMetric {
	return clientGoCacheListTotalMetric
}
func (clientGoCacheMetricsProvider) NewListDurationMetric(name string) cache.SummaryMetric {
	return clientGoCacheListDurationMetric
}
func (clientGoCacheMetricsProvider) NewItemsInListMetric(name string) cache.SummaryMetric {
	return clientGoCacheItemsInListCountMetric
}
func (clientGoCacheMetricsProvider) NewWatchesMetric(name string) cache.CounterMetric {
	return clientGoCacheWatchesCountMetric
}
func (clientGoCacheMetricsProvider) NewShortWatchesMetric(name string) cache.CounterMetric {
	return clientGoCacheShortWatchesCountMetric
}
func (clientGoCacheMetricsProvider) NewWatchDurationMetric(name string) cache.SummaryMetric {
	return clientGoCacheWatchesDurationMetric
}
func (clientGoCacheMetricsProvider) NewItemsInWatchMetric(name string) cache.SummaryMetric {
	return clientGoCacheItemsInWatchesCountMetric
}
func (clientGoCacheMetricsProvider) NewLastResourceVersionMetric(name string) cache.GaugeMetric {
	return clientGoCacheLastResourceVersionMetric
}

// Definition of client-go workqueue metrics provider definition
type clientGoWorkqueueMetricsProvider struct{}

func (f *clientGoWorkqueueMetricsProvider) Register(registerer prometheus.Registerer) {
	workqueue.SetProvider(f)
	registerer.MustRegister(clientGoWorkqueueDepthMetricVec)
	registerer.MustRegister(clientGoWorkqueueAddsMetricVec)
	registerer.MustRegister(clientGoWorkqueueLatencyMetricVec)
	registerer.MustRegister(clientGoWorkqueueWorkDurationMetricVec)
	registerer.MustRegister(clientGoWorkqueueUnfinishedWorkSecondsMetricVec)
	registerer.MustRegister(clientGoWorkqueueLongestRunningProcessorMetricVec)
}

func (f *clientGoWorkqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric {
	return clientGoWorkqueueDepthMetricVec.WithLabelValues(name)
}
func (f *clientGoWorkqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric {
	return clientGoWorkqueueAddsMetricVec.WithLabelValues(name)
}
func (f *clientGoWorkqueueMetricsProvider) NewLatencyMetric(name string) workqueue.SummaryMetric {
	metric := clientGoWorkqueueLatencyMetricVec.WithLabelValues(name)
	// Convert microseconds to seconds for consistency across metrics.
	return prometheus.ObserverFunc(func(v float64) {
		metric.Observe(v / 1e6)
	})
}
func (f *clientGoWorkqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.SummaryMetric {
	metric := clientGoWorkqueueWorkDurationMetricVec.WithLabelValues(name)
	// Convert microseconds to seconds for consistency across metrics.
	return prometheus.ObserverFunc(func(v float64) {
		metric.Observe(v / 1e6)
	})
}
func (f *clientGoWorkqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric {
	return clientGoWorkqueueUnfinishedWorkSecondsMetricVec.WithLabelValues(name)
}
func (f *clientGoWorkqueueMetricsProvider) NewLongestRunningProcessorMicrosecondsMetric(name string) workqueue.SettableGaugeMetric {
	metric := clientGoWorkqueueLongestRunningProcessorMetricVec.WithLabelValues(name)
	return gaugeSetFunc(func(v float64) {
		metric.Set(v / 1e6)
	})

}
func (clientGoWorkqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric {
	// Retries are not used so the metric is omitted.
	return noopMetric{}
}
<commit_msg>address review comment in client_metrics<commit_after>// Copyright 2018 The Prometheus 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 kubernetes

import (
	"net/url"
	"time"

	"github.com/prometheus/client_golang/prometheus"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/metrics"
	"k8s.io/client-go/util/workqueue"
)

const (
	cacheMetricsNamespace     = metricsNamespace + "_cache"
	workqueueMetricsNamespace = metricsNamespace + "_workqueue"
)

var (
	// Metrics for client-go's HTTP requests.
	clientGoRequestResultMetricVec = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: metricsNamespace,
			Name:      "http_request_total",
			Help:      "Total number of HTTP requests to the Kubernetes API by status code.",
		},
		[]string{"status_code"},
	)
	clientGoRequestLatencyMetricVec = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace:  metricsNamespace,
			Name:       "http_request_duration_seconds",
			Help:       "Summary of latencies for HTTP requests to the Kubernetes API by endpoint.",
			Objectives: map[float64]float64{},
		},
		[]string{"endpoint"},
	)

	// Definition of metrics for client-go cache metrics provider.
	clientGoCacheListTotalMetric = prometheus.NewCounter(
		prometheus.CounterOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "list_total",
			Help:      "Total number of list operations.",
		},
	)
	clientGoCacheListDurationMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "list_duration_seconds",
			Help:       "Duration of a Kubernetes API call in seconds.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheItemsInListCountMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "list_items",
			Help:       "Count of items in a list from the Kubernetes API.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheWatchesCountMetric = prometheus.NewCounter(
		prometheus.CounterOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "watches_total",
			Help:      "Total number of watch operations.",
		},
	)
	clientGoCacheShortWatchesCountMetric = prometheus.NewCounter(
		prometheus.CounterOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "short_watches_total",
			Help:      "Total number of short watch operations.",
		},
	)
	clientGoCacheWatchesDurationMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "watch_duration_seconds",
			Help:       "Duration of watches on the Kubernetes API.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheItemsInWatchesCountMetric = prometheus.NewSummary(
		prometheus.SummaryOpts{
			Namespace:  cacheMetricsNamespace,
			Name:       "watch_events",
			Help:       "Number of items in watches on the Kubernetes API.",
			Objectives: map[float64]float64{},
		},
	)
	clientGoCacheLastResourceVersionMetric = prometheus.NewGauge(
		prometheus.GaugeOpts{
			Namespace: cacheMetricsNamespace,
			Name:      "last_resource_version",
			Help:      "Last resource version from the Kubernetes API.",
		},
	)

	// Definition of metrics for client-go workflow metrics provider
	clientGoWorkqueueDepthMetricVec = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "depth",
			Help:      "Current depth of the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueAddsMetricVec = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "items_total",
			Help:      "Total number of items added to the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueLatencyMetricVec = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace:  workqueueMetricsNamespace,
			Name:       "latency_seconds",
			Help:       "How long an item stays in the work queue.",
			Objectives: map[float64]float64{},
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueUnfinishedWorkSecondsMetricVec = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "unfinished_work_seconds",
			Help:      "How long an item has remained unfinished in the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueLongestRunningProcessorMetricVec = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Namespace: workqueueMetricsNamespace,
			Name:      "longest_running_processor_seconds",
			Help:      "Duration of the longest running processor in the work queue.",
		},
		[]string{"queue_name"},
	)
	clientGoWorkqueueWorkDurationMetricVec = prometheus.NewSummaryVec(
		prometheus.SummaryOpts{
			Namespace:  workqueueMetricsNamespace,
			Name:       "work_duration_seconds",
			Help:       "How long processing an item from the work queue takes.",
			Objectives: map[float64]float64{},
		},
		[]string{"queue_name"},
	)
)

// gaugeSetFunc is an adapter that allows the use of functions as gauge metric setters.
type gaugeSetFunc func(float64)

func (s gaugeSetFunc) Set(value float64) {
	s(value)
}

// Definition of dummy metric used as a placeholder if we don't want to observe some data.
type noopMetric struct{}

func (noopMetric) Inc()            {}
func (noopMetric) Dec()            {}
func (noopMetric) Observe(float64) {}

// Definition of client-go metrics adapters for HTTP requests observation
type clientGoRequestMetricAdapter struct{}

func (f *clientGoRequestMetricAdapter) Register(registerer prometheus.Registerer) {
	metrics.Register(f, f)
	registerer.MustRegister(clientGoRequestResultMetricVec)
	registerer.MustRegister(clientGoRequestLatencyMetricVec)
}
func (clientGoRequestMetricAdapter) Increment(code string, method string, host string) {
	clientGoRequestResultMetricVec.WithLabelValues(code).Inc()
}
func (clientGoRequestMetricAdapter) Observe(verb string, u url.URL, latency time.Duration) {
	clientGoRequestLatencyMetricVec.WithLabelValues(u.EscapedPath()).Observe(latency.Seconds())
}

// Definition of client-go cache metrics provider definition
type clientGoCacheMetricsProvider struct{}

func (f *clientGoCacheMetricsProvider) Register(registerer prometheus.Registerer) {
	cache.SetReflectorMetricsProvider(f)
	registerer.MustRegister(clientGoCacheWatchesDurationMetric)
	registerer.MustRegister(clientGoCacheWatchesCountMetric)
	registerer.MustRegister(clientGoCacheListDurationMetric)
	registerer.MustRegister(clientGoCacheListTotalMetric)
	registerer.MustRegister(clientGoCacheLastResourceVersionMetric)
	registerer.MustRegister(clientGoCacheShortWatchesCountMetric)
	registerer.MustRegister(clientGoCacheItemsInWatchesCountMetric)
	registerer.MustRegister(clientGoCacheItemsInListCountMetric)

}
func (clientGoCacheMetricsProvider) NewListsMetric(name string) cache.CounterMetric {
	return clientGoCacheListTotalMetric
}
func (clientGoCacheMetricsProvider) NewListDurationMetric(name string) cache.SummaryMetric {
	return clientGoCacheListDurationMetric
}
func (clientGoCacheMetricsProvider) NewItemsInListMetric(name string) cache.SummaryMetric {
	return clientGoCacheItemsInListCountMetric
}
func (clientGoCacheMetricsProvider) NewWatchesMetric(name string) cache.CounterMetric {
	return clientGoCacheWatchesCountMetric
}
func (clientGoCacheMetricsProvider) NewShortWatchesMetric(name string) cache.CounterMetric {
	return clientGoCacheShortWatchesCountMetric
}
func (clientGoCacheMetricsProvider) NewWatchDurationMetric(name string) cache.SummaryMetric {
	return clientGoCacheWatchesDurationMetric
}
func (clientGoCacheMetricsProvider) NewItemsInWatchMetric(name string) cache.SummaryMetric {
	return clientGoCacheItemsInWatchesCountMetric
}
func (clientGoCacheMetricsProvider) NewLastResourceVersionMetric(name string) cache.GaugeMetric {
	return clientGoCacheLastResourceVersionMetric
}

// Definition of client-go workqueue metrics provider definition
type clientGoWorkqueueMetricsProvider struct{}

func (f *clientGoWorkqueueMetricsProvider) Register(registerer prometheus.Registerer) {
	workqueue.SetProvider(f)
	registerer.MustRegister(clientGoWorkqueueDepthMetricVec)
	registerer.MustRegister(clientGoWorkqueueAddsMetricVec)
	registerer.MustRegister(clientGoWorkqueueLatencyMetricVec)
	registerer.MustRegister(clientGoWorkqueueWorkDurationMetricVec)
	registerer.MustRegister(clientGoWorkqueueUnfinishedWorkSecondsMetricVec)
	registerer.MustRegister(clientGoWorkqueueLongestRunningProcessorMetricVec)
}

func (f *clientGoWorkqueueMetricsProvider) NewDepthMetric(name string) workqueue.GaugeMetric {
	return clientGoWorkqueueDepthMetricVec.WithLabelValues(name)
}
func (f *clientGoWorkqueueMetricsProvider) NewAddsMetric(name string) workqueue.CounterMetric {
	return clientGoWorkqueueAddsMetricVec.WithLabelValues(name)
}
func (f *clientGoWorkqueueMetricsProvider) NewLatencyMetric(name string) workqueue.SummaryMetric {
	metric := clientGoWorkqueueLatencyMetricVec.WithLabelValues(name)
	// Convert microseconds to seconds for consistency across metrics.
	return prometheus.ObserverFunc(func(v float64) {
		metric.Observe(v / 1e6)
	})
}
func (f *clientGoWorkqueueMetricsProvider) NewWorkDurationMetric(name string) workqueue.SummaryMetric {
	metric := clientGoWorkqueueWorkDurationMetricVec.WithLabelValues(name)
	// Convert microseconds to seconds for consistency across metrics.
	return prometheus.ObserverFunc(func(v float64) {
		metric.Observe(v / 1e6)
	})
}
func (f *clientGoWorkqueueMetricsProvider) NewUnfinishedWorkSecondsMetric(name string) workqueue.SettableGaugeMetric {
	return clientGoWorkqueueUnfinishedWorkSecondsMetricVec.WithLabelValues(name)
}
func (f *clientGoWorkqueueMetricsProvider) NewLongestRunningProcessorMicrosecondsMetric(name string) workqueue.SettableGaugeMetric {
	metric := clientGoWorkqueueLongestRunningProcessorMetricVec.WithLabelValues(name)
	return gaugeSetFunc(func(v float64) {
		metric.Set(v / 1e6)
	})

}
func (clientGoWorkqueueMetricsProvider) NewRetriesMetric(name string) workqueue.CounterMetric {
	// Retries are not used so the metric is omitted.
	return noopMetric{}
}
<|endoftext|>


<commit_before>// Copyright 2018 Diego Bernardes. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package worker

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"

	"github.com/pkg/errors"

	"github.com/diegobernardes/flare"
	infraURL "github.com/diegobernardes/flare/infra/url"
	"github.com/diegobernardes/flare/infra/wildcard"
	"github.com/diegobernardes/flare/infra/worker"
)

// Delivery do the heavy lifting by discovering if the given subscription need or not to receive the
// document.
type Delivery struct {
	pusher                 worker.Pusher
	resourceRepository     flare.ResourceRepositorier
	subscriptionRepository flare.SubscriptionRepositorier
	httpClient             *http.Client
}

// Push the signal to delivery the document.
func (d *Delivery) Push(
	ctx context.Context, subscription *flare.Subscription, document *flare.Document, action string,
) error {
	content, err := d.marshal(subscription, document, action)
	if err != nil {
		return errors.Wrap(err, "error during trigger")
	}

	if err = d.pusher.Push(ctx, content); err != nil {
		return errors.Wrap(err, "error during message delivery")
	}
	return nil
}

// Process the message.
func (d *Delivery) Process(ctx context.Context, rawContent []byte) error {
	subscription, document, action, err := d.unmarshal(rawContent)
	if err != nil {
		return errors.Wrap(err, "error during content unmarshal")
	}

	err = d.subscriptionRepository.Trigger(ctx, action, document, subscription, d.trigger)
	if err != nil {
		return errors.Wrap(err, "error during subscription trigger")
	}
	return nil
}

// Init initialize the Delivery.
func (d *Delivery) Init(options ...func(*Delivery)) error {
	for _, option := range options {
		option(d)
	}

	if d.pusher == nil {
		return errors.New("pusher not found")
	}

	if d.resourceRepository == nil {
		return errors.New("resource repository not found")
	}

	if d.subscriptionRepository == nil {
		return errors.New("subscription repository not found")
	}

	if d.httpClient == nil {
		return errors.New("httpClient not found")
	}

	return nil
}

func (d *Delivery) marshal(
	subscription *flare.Subscription, document *flare.Document, action string,
) ([]byte, error) {
	id, err := infraURL.String(document.ID)
	if err != nil {
		return nil, errors.Wrap(err, "error during document.ID unmarshal")
	}

	rawContent := struct {
		Action         string `json:"action"`
		DocumentID     string `json:"documentID"`
		ResourceID     string `json:"resourceID"`
		SubscriptionID string `json:"subscriptionID"`
	}{
		Action:         action,
		DocumentID:     id,
		ResourceID:     document.Resource.ID,
		SubscriptionID: subscription.ID,
	}

	content, err := json.Marshal(rawContent)
	if err != nil {
		return nil, errors.Wrap(err, "error during message marshal")
	}
	return content, nil
}

func (d *Delivery) unmarshal(
	rawContent []byte,
) (*flare.Subscription, *flare.Document, string, error) {
	type content struct {
		Action         string `json:"action"`
		DocumentID     string `json:"documentID"`
		ResourceID     string `json:"resourceID"`
		SubscriptionID string `json:"subscriptionID"`
	}

	var value content
	if err := json.Unmarshal(rawContent, &value); err != nil {
		return nil, nil, "", errors.Wrap(err, "error during content unmarshal")
	}

	id, err := url.Parse(value.DocumentID)
	if err != nil {
		return nil, nil, "", errors.Wrap(err, "error during parse documentID")
	}

	return &flare.Subscription{ID: value.SubscriptionID},
		&flare.Document{ID: *id, Resource: flare.Resource{ID: value.ResourceID}},
		value.Action,
		nil
}

func (d *Delivery) buildContent(
	resource *flare.Resource,
	document *flare.Document,
	documentEndpoint *url.URL,
	sub flare.Subscription,
	kind string,
) ([]byte, error) {
	var content map[string]interface{}

	if !sub.Content.Envelope {
		content = document.Content
	} else {
		id, err := infraURL.String(document.ID)
		if err != nil {
			return nil, errors.Wrap(err, "error during document.ID unmarshal")
		}

		content = map[string]interface{}{
			"id":        id,
			"action":    kind,
			"updatedAt": document.UpdatedAt.String(),
		}
		if len(sub.Data) > 0 {
			values := wildcard.ExtractValue(resource.Endpoint.Path, documentEndpoint.Path)

			for key, rawValue := range sub.Data {
				value, ok := rawValue.(string)
				if !ok {
					continue
				}
				sub.Data[key] = wildcard.Replace(value, values)
			}

			content["data"] = sub.Data
		}

		if sub.Content.Document {
			content["document"] = document.Content
		}
	}

	result, err := json.Marshal(content)
	if err != nil {
		return nil, errors.Wrap(err, "error during response generate")
	}
	return result, nil
}

func (d *Delivery) trigger(
	ctx context.Context,
	document *flare.Document,
	subscription *flare.Subscription,
	action string,
) error {
	req, err := d.buildRequest(ctx, document, subscription, action)
	if err != nil {
		return err
	}

	resp, err := d.httpClient.Do(req)
	if err != nil {
		return errors.Wrap(err, "error during http request")
	}

	for _, status := range subscription.Delivery.Success {
		if status == resp.StatusCode {
			return nil
		}
	}

	for _, status := range subscription.Delivery.Discard {
		if status == resp.StatusCode {
			return nil
		}
	}

	return errors.Errorf(
		"success and discard status don't match with the response value '%d'", resp.StatusCode,
	)
}

func (d *Delivery) buildRequest(
	ctx context.Context,
	document *flare.Document,
	subscription *flare.Subscription,
	action string,
) (*http.Request, error) {
	resource, err := d.resourceRepository.FindByID(ctx, document.Resource.ID)
	if err != nil {
		return nil, err
	}

	content, err := d.buildContent(resource, document, &document.ID, *subscription, action)
	if err != nil {
		return nil, errors.Wrap(err, "error during content build")
	}

	rawAddr := subscription.Endpoint.URL
	headers := subscription.Endpoint.Headers
	method := subscription.Endpoint.Method
	endpointAction, ok := subscription.Endpoint.Action[action]
	if ok {
		if endpointAction.Method != "" {
			method = endpointAction.Method
		}

		if len(endpointAction.Headers) > 0 {
			headers = endpointAction.Headers
		}

		if endpointAction.URL != nil {
			rawAddr = endpointAction.URL
		}
	}

	addr, err := d.buildEndpoint(resource, &document.ID, rawAddr)
	if err != nil {
		return nil, errors.Wrap(err, "error during endpoint generate")
	}

	req, err := d.buildRequestHTTP(ctx, content, addr, method, headers)
	if err != nil {
		return nil, err
	}

	return req, nil
}

func (d *Delivery) buildRequestHTTP(
	ctx context.Context,
	content []byte,
	addr, method string,
	headers http.Header,
) (*http.Request, error) {
	buf := bytes.NewBuffer(content)
	req, err := http.NewRequest(method, addr, buf)
	if err != nil {
		return nil, errors.Wrap(err, "error during http request create")
	}
	req = req.WithContext(ctx)

	req.Header = headers
	contentType := req.Header.Get("content-type")
	if contentType == "" && len(content) > 0 {
		req.Header.Add("Content-Type", "application/json")
	}

	return req, nil
}

func (d *Delivery) buildEndpoint(
	resource *flare.Resource,
	endpoint *url.URL,
	rawSubscriptionEndpoint fmt.Stringer,
) (string, error) {
	values := wildcard.ExtractValue(resource.Endpoint.Path, endpoint.Path)
	subscriptionEndpoint, err := url.QueryUnescape(rawSubscriptionEndpoint.String())
	if err != nil {
		return "", errors.Wrap(err, "error during subscription endpoint unescape")
	}

	return wildcard.Replace(subscriptionEndpoint, values), nil
}

// DeliveryResourceRepository set the resource repository.
func DeliveryResourceRepository(repository flare.ResourceRepositorier) func(*Delivery) {
	return func(d *Delivery) { d.resourceRepository = repository }
}

// DeliverySubscriptionRepository set the subscription repository.
func DeliverySubscriptionRepository(repository flare.SubscriptionRepositorier) func(*Delivery) {
	return func(d *Delivery) { d.subscriptionRepository = repository }
}

// DeliveryPusher set the output of the messages.
func DeliveryPusher(pusher worker.Pusher) func(*Delivery) {
	return func(d *Delivery) { d.pusher = pusher }
}

// DeliveryHTTPClient set the default HTTP client to send the document changes.
func DeliveryHTTPClient(client *http.Client) func(*Delivery) {
	return func(d *Delivery) { d.httpClient = client }
}
<commit_msg>subscription: initialize headers struct before use<commit_after>// Copyright 2018 Diego Bernardes. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package worker

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"

	"github.com/pkg/errors"

	"github.com/diegobernardes/flare"
	infraURL "github.com/diegobernardes/flare/infra/url"
	"github.com/diegobernardes/flare/infra/wildcard"
	"github.com/diegobernardes/flare/infra/worker"
)

// Delivery do the heavy lifting by discovering if the given subscription need or not to receive the
// document.
type Delivery struct {
	pusher                 worker.Pusher
	resourceRepository     flare.ResourceRepositorier
	subscriptionRepository flare.SubscriptionRepositorier
	httpClient             *http.Client
}

// Push the signal to delivery the document.
func (d *Delivery) Push(
	ctx context.Context, subscription *flare.Subscription, document *flare.Document, action string,
) error {
	content, err := d.marshal(subscription, document, action)
	if err != nil {
		return errors.Wrap(err, "error during trigger")
	}

	if err = d.pusher.Push(ctx, content); err != nil {
		return errors.Wrap(err, "error during message delivery")
	}
	return nil
}

// Process the message.
func (d *Delivery) Process(ctx context.Context, rawContent []byte) error {
	subscription, document, action, err := d.unmarshal(rawContent)
	if err != nil {
		return errors.Wrap(err, "error during content unmarshal")
	}

	err = d.subscriptionRepository.Trigger(ctx, action, document, subscription, d.trigger)
	if err != nil {
		return errors.Wrap(err, "error during subscription trigger")
	}
	return nil
}

// Init initialize the Delivery.
func (d *Delivery) Init(options ...func(*Delivery)) error {
	for _, option := range options {
		option(d)
	}

	if d.pusher == nil {
		return errors.New("pusher not found")
	}

	if d.resourceRepository == nil {
		return errors.New("resource repository not found")
	}

	if d.subscriptionRepository == nil {
		return errors.New("subscription repository not found")
	}

	if d.httpClient == nil {
		return errors.New("httpClient not found")
	}

	return nil
}

func (d *Delivery) marshal(
	subscription *flare.Subscription, document *flare.Document, action string,
) ([]byte, error) {
	id, err := infraURL.String(document.ID)
	if err != nil {
		return nil, errors.Wrap(err, "error during document.ID unmarshal")
	}

	rawContent := struct {
		Action         string `json:"action"`
		DocumentID     string `json:"documentID"`
		ResourceID     string `json:"resourceID"`
		SubscriptionID string `json:"subscriptionID"`
	}{
		Action:         action,
		DocumentID:     id,
		ResourceID:     document.Resource.ID,
		SubscriptionID: subscription.ID,
	}

	content, err := json.Marshal(rawContent)
	if err != nil {
		return nil, errors.Wrap(err, "error during message marshal")
	}
	return content, nil
}

func (d *Delivery) unmarshal(
	rawContent []byte,
) (*flare.Subscription, *flare.Document, string, error) {
	type content struct {
		Action         string `json:"action"`
		DocumentID     string `json:"documentID"`
		ResourceID     string `json:"resourceID"`
		SubscriptionID string `json:"subscriptionID"`
	}

	var value content
	if err := json.Unmarshal(rawContent, &value); err != nil {
		return nil, nil, "", errors.Wrap(err, "error during content unmarshal")
	}

	id, err := url.Parse(value.DocumentID)
	if err != nil {
		return nil, nil, "", errors.Wrap(err, "error during parse documentID")
	}

	return &flare.Subscription{ID: value.SubscriptionID},
		&flare.Document{ID: *id, Resource: flare.Resource{ID: value.ResourceID}},
		value.Action,
		nil
}

func (d *Delivery) buildContent(
	resource *flare.Resource,
	document *flare.Document,
	documentEndpoint *url.URL,
	sub flare.Subscription,
	kind string,
) ([]byte, error) {
	var content map[string]interface{}

	if !sub.Content.Envelope {
		content = document.Content
	} else {
		id, err := infraURL.String(document.ID)
		if err != nil {
			return nil, errors.Wrap(err, "error during document.ID unmarshal")
		}

		content = map[string]interface{}{
			"id":        id,
			"action":    kind,
			"updatedAt": document.UpdatedAt.String(),
		}
		if len(sub.Data) > 0 {
			values := wildcard.ExtractValue(resource.Endpoint.Path, documentEndpoint.Path)

			for key, rawValue := range sub.Data {
				value, ok := rawValue.(string)
				if !ok {
					continue
				}
				sub.Data[key] = wildcard.Replace(value, values)
			}

			content["data"] = sub.Data
		}

		if sub.Content.Document {
			content["document"] = document.Content
		}
	}

	result, err := json.Marshal(content)
	if err != nil {
		return nil, errors.Wrap(err, "error during response generate")
	}
	return result, nil
}

func (d *Delivery) trigger(
	ctx context.Context,
	document *flare.Document,
	subscription *flare.Subscription,
	action string,
) error {
	req, err := d.buildRequest(ctx, document, subscription, action)
	if err != nil {
		return err
	}

	resp, err := d.httpClient.Do(req)
	if err != nil {
		return errors.Wrap(err, "error during http request")
	}

	for _, status := range subscription.Delivery.Success {
		if status == resp.StatusCode {
			return nil
		}
	}

	for _, status := range subscription.Delivery.Discard {
		if status == resp.StatusCode {
			return nil
		}
	}

	return errors.Errorf(
		"success and discard status don't match with the response value '%d'", resp.StatusCode,
	)
}

func (d *Delivery) buildRequest(
	ctx context.Context,
	document *flare.Document,
	subscription *flare.Subscription,
	action string,
) (*http.Request, error) {
	resource, err := d.resourceRepository.FindByID(ctx, document.Resource.ID)
	if err != nil {
		return nil, err
	}

	content, err := d.buildContent(resource, document, &document.ID, *subscription, action)
	if err != nil {
		return nil, errors.Wrap(err, "error during content build")
	}

	rawAddr := subscription.Endpoint.URL
	headers := subscription.Endpoint.Headers
	method := subscription.Endpoint.Method
	endpointAction, ok := subscription.Endpoint.Action[action]
	if ok {
		if endpointAction.Method != "" {
			method = endpointAction.Method
		}

		if len(endpointAction.Headers) > 0 {
			headers = endpointAction.Headers
		}

		if endpointAction.URL != nil {
			rawAddr = endpointAction.URL
		}
	}

	addr, err := d.buildEndpoint(resource, &document.ID, rawAddr)
	if err != nil {
		return nil, errors.Wrap(err, "error during endpoint generate")
	}

	req, err := d.buildRequestHTTP(ctx, content, addr, method, headers)
	if err != nil {
		return nil, err
	}

	return req, nil
}

func (d *Delivery) buildRequestHTTP(
	ctx context.Context,
	content []byte,
	addr, method string,
	headers http.Header,
) (*http.Request, error) {
	buf := bytes.NewBuffer(content)
	req, err := http.NewRequest(method, addr, buf)
	if err != nil {
		return nil, errors.Wrap(err, "error during http request create")
	}
	req = req.WithContext(ctx)

	req.Header = headers

	if req.Header == nil {
		req.Header = make(http.Header)
	}

	contentType := req.Header.Get("content-type")
	if contentType == "" && len(content) > 0 {
		req.Header.Add("Content-Type", "application/json")
	}

	return req, nil
}

func (d *Delivery) buildEndpoint(
	resource *flare.Resource,
	endpoint *url.URL,
	rawSubscriptionEndpoint fmt.Stringer,
) (string, error) {
	values := wildcard.ExtractValue(resource.Endpoint.Path, endpoint.Path)
	subscriptionEndpoint, err := url.QueryUnescape(rawSubscriptionEndpoint.String())
	if err != nil {
		return "", errors.Wrap(err, "error during subscription endpoint unescape")
	}

	return wildcard.Replace(subscriptionEndpoint, values), nil
}

// DeliveryResourceRepository set the resource repository.
func DeliveryResourceRepository(repository flare.ResourceRepositorier) func(*Delivery) {
	return func(d *Delivery) { d.resourceRepository = repository }
}

// DeliverySubscriptionRepository set the subscription repository.
func DeliverySubscriptionRepository(repository flare.SubscriptionRepositorier) func(*Delivery) {
	return func(d *Delivery) { d.subscriptionRepository = repository }
}

// DeliveryPusher set the output of the messages.
func DeliveryPusher(pusher worker.Pusher) func(*Delivery) {
	return func(d *Delivery) { d.pusher = pusher }
}

// DeliveryHTTPClient set the default HTTP client to send the document changes.
func DeliveryHTTPClient(client *http.Client) func(*Delivery) {
	return func(d *Delivery) { d.httpClient = client }
}
<|endoftext|>