diff --git a/README.md b/README.md index 10aee21..922ba01 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Read More: https://www.civo.com/learn/managing-external-load-balancers-on-civo | kubernetes.civo.com/firewall-id | If provided, an existing Firewall will be used. | 03093EF6-31E6-48B1-AB1D-152AC3A8C90A | | kubernetes.civo.com/loadbalancer-enable-proxy-protocol | If set, a proxy-protocol header will be sent via the load balancer.

NB: This requires support from the Service End Points within the cluster. | send-proxy
send-proxy-v2 | | kubernetes.civo.com/loadbalancer-algorithm | Custom the algorithm the external load balancer uses | round_robin
least_connections | +|kubernetes.civo.com/ipv4-address| If set, LoadBalancer will have the mentioned IP as the public IP. Please note: the reserved IP should be present in the account before claiming it. | 10.0.0.20
my-reserved-ip | ### Load Balancer Status Annotations diff --git a/cloud-controller-manager/civo/cloud.go b/cloud-controller-manager/civo/cloud.go index ebb0dcd..ea390ee 100644 --- a/cloud-controller-manager/civo/cloud.go +++ b/cloud-controller-manager/civo/cloud.go @@ -9,14 +9,20 @@ import ( ) const ( + // ProviderName is the name of the provider. ProviderName string = "civo" ) var ( - ApiURL string - ApiKey string - Region string + // APIURL is the URL of the Civo API. + APIURL string + // APIKey is the API key for the Civo API. + APIKey string + // Region is the region of the Civo API. + Region string + // Namespace of the cluster Namespace string + // ClusterID is the ID of the Civo cluster. ClusterID string ) @@ -33,7 +39,7 @@ func init() { } func newCloud() (cloudprovider.Interface, error) { - client, err := civogo.NewClientWithURL(ApiKey, ApiURL, Region) + client, err := civogo.NewClientWithURL(APIKey, APIURL, Region) if err != nil { return nil, err } diff --git a/cloud-controller-manager/civo/loadbalancer.go b/cloud-controller-manager/civo/loadbalancer.go index a1cc126..6d5f5ce 100644 --- a/cloud-controller-manager/civo/loadbalancer.go +++ b/cloud-controller-manager/civo/loadbalancer.go @@ -34,6 +34,9 @@ const ( // annotationCivoLoadBalancerAlgorithm is the annotation specifying the CivoLoadbalancer algorith. annotationCivoLoadBalancerAlgorithm = "kubernetes.civo.com/loadbalancer-algorithm" + + // annotationCivoIPv4 is the annotation specifying the reserved IP. + annotationCivoIPv4 = "kubernetes.civo.com/ipv4-address" ) type loadbalancer struct { @@ -88,30 +91,15 @@ func (l *loadbalancer) EnsureLoadBalancer(ctx context.Context, clusterName strin return nil, err } - // CivLB has been found + // CivoLB has been found if err == nil { - lbuc := civogo.LoadBalancerUpdateConfig{ - ExternalTrafficPolicy: string(service.Spec.ExternalTrafficPolicy), - Region: Region, - } - - if enableProxyProtocol := getEnableProxyProtocol(service); enableProxyProtocol != "" { - lbuc.EnableProxyProtocol = enableProxyProtocol - } - if algorithm := getAlgorithm(service); algorithm != "" { - lbuc.Algorithm = algorithm - } - if firewallID := getFirewallID(service); firewallID != "" { - lbuc.FirewallID = firewallID - } - - updatedlb, err := l.client.civoClient.UpdateLoadBalancer(civolb.ID, &lbuc) + ul, err := l.updateLBConfig(civolb, service, nodes) if err != nil { klog.Errorf("Unable to update loadbalancer, error: %v", err) return nil, err } - return lbStatusFor(updatedlb), nil + return lbStatusFor(ul) } err = createLoadBalancer(ctx, clusterName, service, nodes, l.client.civoClient, l.client.kclient) @@ -125,41 +113,10 @@ func (l *loadbalancer) EnsureLoadBalancer(ctx context.Context, clusterName strin return nil, err } - if civolb.State != statusAvailable { - klog.Errorf("Loadbalancer is not available, state: %s", civolb.State) - return nil, fmt.Errorf("loadbalancer is not yet available, current state: %s", civolb.State) - } - - return lbStatusFor(civolb), nil + return lbStatusFor(civolb) } -func lbStatusFor(civolb *civogo.LoadBalancer) *v1.LoadBalancerStatus { - status := &v1.LoadBalancerStatus{ - Ingress: make([]v1.LoadBalancerIngress, 1), - } - - if civolb.EnableProxyProtocol == "" { - status.Ingress[0].IP = civolb.PublicIP - } - status.Ingress[0].Hostname = fmt.Sprintf("%s.lb.civo.com", civolb.ID) - - return status -} - -// UpdateLoadBalancer updates hosts under the specified load balancer. -// Implementations must treat the *v1.Service and *v1.Node -// parameters as read-only and not modify them. -// Parameter 'clusterName' is the name of the cluster as presented to kube-controller-manager -func (l *loadbalancer) UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error { - civolb, err := getLoadBalancer(ctx, l.client.civoClient, l.client.kclient, clusterName, service) - if err != nil { - if strings.Contains(err.Error(), string(civogo.ZeroMatchesError)) || strings.Contains(err.Error(), string(civogo.DatabaseLoadBalancerNotFoundError)) { - return nil - } - klog.Errorf("Unable to get loadbalancer, error: %v", err) - return err - } - +func (l *loadbalancer) updateLBConfig(civolb *civogo.LoadBalancer, service *v1.Service, nodes []*v1.Node) (*civogo.LoadBalancer, error) { lbuc := civogo.LoadBalancerUpdateConfig{ ExternalTrafficPolicy: string(service.Spec.ExternalTrafficPolicy), Region: Region, @@ -189,7 +146,97 @@ func (l *loadbalancer) UpdateLoadBalancer(ctx context.Context, clusterName strin } lbuc.Backends = backends - ulb, err := l.client.civoClient.UpdateLoadBalancer(civolb.ID, &lbuc) + if ip := getReservedIPFromAnnotation(service); ip != "" { + rip, err := l.client.civoClient.FindIP(ip) + if err != nil { + klog.Errorf("Unable to find reserved IP, error: %v", err) + return nil, err + } + + // this is so that we don't try to reassign the reserved IP to the loadbalancer + if rip.AssignedTo.ID != civolb.ID { + _, err = l.client.civoClient.AssignIP(rip.ID, civolb.ID, "loadbalancer") + if err != nil { + klog.Errorf("Unable to assign reserved IP, error: %v", err) + return nil, err + } + } + } else { + ip, err := findIPWithLBID(l.client.civoClient, civolb.ID) + if err != nil { + klog.Errorf("Unable to find IP with loadbalancer ID, error: %v", err) + return nil, err + } + + if ip != nil { + _, err = l.client.civoClient.UnassignIP(ip.ID) + if err != nil { + klog.Errorf("Unable to unassign IP, error: %v", err) + return nil, err + } + } + } + + updatedlb, err := l.client.civoClient.UpdateLoadBalancer(civolb.ID, &lbuc) + if err != nil { + klog.Errorf("Unable to update loadbalancer, error: %v", err) + return nil, err + } + + return updatedlb, nil + +} + +// there's no direct way to find if the LB is using a reserved IP. This method lists all the reserved IPs in the account +// and checks if the loadbalancer is using one of them. +func findIPWithLBID(civo civogo.Clienter, lbID string) (*civogo.IP, error) { + ips, err := civo.ListIPs() + if err != nil { + klog.Errorf("Unable to list IPs, error: %v", err) + return nil, err + } + + for _, ip := range ips.Items { + if ip.AssignedTo.ID == lbID { + return &ip, nil + } + } + return nil, nil +} + +func lbStatusFor(civolb *civogo.LoadBalancer) (*v1.LoadBalancerStatus, error) { + status := &v1.LoadBalancerStatus{ + Ingress: make([]v1.LoadBalancerIngress, 1), + } + + if civolb.State != statusAvailable { + klog.Errorf("Loadbalancer is not available, state: %s", civolb.State) + return nil, fmt.Errorf("loadbalancer is not yet available, current state: %s", civolb.State) + } + + if civolb.EnableProxyProtocol == "" { + status.Ingress[0].IP = civolb.PublicIP + } + status.Ingress[0].Hostname = fmt.Sprintf("%s.lb.civo.com", civolb.ID) + + return status, nil +} + +// UpdateLoadBalancer updates hosts under the specified load balancer. +// Implementations must treat the *v1.Service and *v1.Node +// parameters as read-only and not modify them. +// Parameter 'clusterName' is the name of the cluster as presented to kube-controller-manager +func (l *loadbalancer) UpdateLoadBalancer(ctx context.Context, clusterName string, service *v1.Service, nodes []*v1.Node) error { + civolb, err := getLoadBalancer(ctx, l.client.civoClient, l.client.kclient, clusterName, service) + if err != nil { + if strings.Contains(err.Error(), string(civogo.ZeroMatchesError)) || strings.Contains(err.Error(), string(civogo.DatabaseLoadBalancerNotFoundError)) { + return nil + } + klog.Errorf("Unable to get loadbalancer, error: %v", err) + return err + } + + ulb, err := l.updateLBConfig(civolb, service, nodes) if err != nil { klog.Errorf("Unable to update loadbalancer, error: %v", err) return err @@ -350,20 +397,19 @@ func getEnableProxyProtocol(service *v1.Service) string { // getAlgorithm returns the algorithm value from the service annotation. func getAlgorithm(service *v1.Service) string { - algorithm, ok := service.Annotations[annotationCivoLoadBalancerAlgorithm] - if !ok { - return "" - } + algorithm, _ := service.Annotations[annotationCivoLoadBalancerAlgorithm] return algorithm } +// getReservedIPFromAnnotation returns the reservedIP value from the service annotation. +func getReservedIPFromAnnotation(service *v1.Service) string { + ip, _ := service.Annotations[annotationCivoIPv4] + return ip +} + // getFirewallID returns the firewallID value from the service annotation. func getFirewallID(service *v1.Service) string { - firewallID, ok := service.Annotations[annotationCivoFirewallID] - if !ok { - return "" - } - + firewallID, _ := service.Annotations[annotationCivoFirewallID] return firewallID } diff --git a/cloud-controller-manager/cmd/civo-cloud-controller-manager/main.go b/cloud-controller-manager/cmd/civo-cloud-controller-manager/main.go index 2e296a8..b76d4c5 100644 --- a/cloud-controller-manager/cmd/civo-cloud-controller-manager/main.go +++ b/cloud-controller-manager/cmd/civo-cloud-controller-manager/main.go @@ -18,17 +18,17 @@ import ( func main() { - civo.ApiURL = os.Getenv("CIVO_API_URL") - civo.ApiKey = os.Getenv("CIVO_API_KEY") + civo.APIURL = os.Getenv("CIVO_API_URL") + civo.APIKey = os.Getenv("CIVO_API_KEY") civo.Region = os.Getenv("CIVO_REGION") civo.ClusterID = os.Getenv("CIVO_CLUSTER_ID") - if civo.ApiURL == "" || civo.ApiKey == "" || civo.Region == "" || civo.ClusterID == "" { + if civo.APIURL == "" || civo.APIKey == "" || civo.Region == "" || civo.ClusterID == "" { fmt.Println("CIVO_API_URL, CIVO_API_KEY, CIVO_REGION, CIVO_CLUSTER_ID environment variables must be set") os.Exit(1) } - klog.Infof("Starting ccm with CIVO_API_URL: %s, CIVO_REGION: %s, CIVO_CLUSTER_ID: %s", civo.ApiURL, civo.Region, civo.ClusterID) + klog.Infof("Starting ccm with CIVO_API_URL: %s, CIVO_REGION: %s, CIVO_CLUSTER_ID: %s", civo.APIURL, civo.Region, civo.ClusterID) opts, err := options.NewCloudControllerManagerOptions() if err != nil { diff --git a/cloud-controller-manager/pkg/utils/instance.go b/cloud-controller-manager/pkg/utils/instance.go index be598d0..98cc16c 100644 --- a/cloud-controller-manager/pkg/utils/instance.go +++ b/cloud-controller-manager/pkg/utils/instance.go @@ -31,6 +31,7 @@ func civoInstanceFromID(clusterID, instanceID string, c civogo.Clienter) (civogo return *instance, nil } +// CivoInstanceFromProviderID finds civo instance by clusterID and providerID func CivoInstanceFromProviderID(providerID, clusterID string, c civogo.Clienter) (civogo.Instance, error) { civoInstanceID, err := civoInstanceIDFromProviderID(providerID) if err != nil { @@ -45,6 +46,7 @@ func CivoInstanceFromProviderID(providerID, clusterID string, c civogo.Clienter) return civoInstance, nil } +// CivoInstanceFromName finds civo instance by clusterID and name func CivoInstanceFromName(clusterID, instanceName string, c civogo.Clienter) (civogo.Instance, error) { instance, err := c.FindKubernetesClusterInstance(clusterID, instanceName) if err != nil { diff --git a/e2e/loadbalacner_test.go b/e2e/loadbalacner_test.go deleted file mode 100644 index c39ef04..0000000 --- a/e2e/loadbalacner_test.go +++ /dev/null @@ -1,163 +0,0 @@ -package test - -import ( - "context" - "fmt" - "testing" - - . "github.com/onsi/gomega" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/util/intstr" -) - -func TestLoadbalacnerBasic(t *testing.T) { - - g := NewGomegaWithT(t) - - mirrorDeploy, err := deployMirrorPods(e2eTest.tenantClient) - g.Expect(err).ShouldNot(HaveOccurred()) - - lbls := map[string]string{"app": "mirror-pod"} - // Create a service of type: LoadBalacner - svc := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "echo-pods", - Namespace: "default", - }, - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{ - {Name: "http", Protocol: "TCP", Port: 80, TargetPort: intstr.FromInt(8080)}, - {Name: "https", Protocol: "TCP", Port: 443, TargetPort: intstr.FromInt(8443)}, - }, - Selector: lbls, - Type: "LoadBalancer", - }, - } - - fmt.Println("Creating Service") - err = e2eTest.tenantClient.Create(context.TODO(), svc) - g.Expect(err).ShouldNot(HaveOccurred()) - - g.Eventually(func() string { - err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) - if len(svc.Status.LoadBalancer.Ingress) == 0 { - return "" - } - return svc.Status.LoadBalancer.Ingress[0].IP - }, "2m", "5s").ShouldNot(BeEmpty()) - - // Cleanup - err = cleanUp(mirrorDeploy, svc) - g.Expect(err).ShouldNot(HaveOccurred()) - - g.Eventually(func() error { - return e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) - }, "2m", "5s").ShouldNot(BeNil()) -} - -func TestLoadbalacnerProxy(t *testing.T) { - g := NewGomegaWithT(t) - - _, err := deployMirrorPods(e2eTest.tenantClient) - g.Expect(err).ShouldNot(HaveOccurred()) - - lbls := map[string]string{"app": "mirror-pod"} - // Create a service of type: LoadBalacner - svc := &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "echo-pods", - Namespace: "default", - Annotations: map[string]string{ - "kubernetes.civo.com/loadbalancer-enable-proxy-protocol": "send-proxy", - }, - }, - Spec: corev1.ServiceSpec{ - Ports: []corev1.ServicePort{ - {Name: "http", Protocol: "TCP", Port: 80, TargetPort: intstr.FromInt(8081)}, - {Name: "https", Protocol: "TCP", Port: 443, TargetPort: intstr.FromInt(8444)}, - }, - Selector: lbls, - Type: "LoadBalancer", - }, - } - - fmt.Println("Creating Service") - err = e2eTest.tenantClient.Create(context.TODO(), svc) - g.Expect(err).ShouldNot(HaveOccurred()) - - g.Eventually(func() string { - err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) - if len(svc.Status.LoadBalancer.Ingress) == 0 { - return "" - } - return svc.Status.LoadBalancer.Ingress[0].IP - }, "2m", "5s").ShouldNot(BeEmpty()) - - /* - // Cleanup - err = cleanUp(mirrorDeploy, svc) - g.Expect(err).ShouldNot(HaveOccurred()) - - g.Eventually(func() error { - return e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) - }, "2m", "5s").ShouldNot(BeNil()) - */ -} - -func cleanUp(mirrorDeploy *appsv1.Deployment, svc *corev1.Service) error { - err := e2eTest.tenantClient.Delete(context.TODO(), svc) - if err != nil { - return err - } - - return e2eTest.tenantClient.Delete(context.TODO(), mirrorDeploy) -} - -func deployMirrorPods(c client.Client) (*appsv1.Deployment, error) { - lbls := map[string]string{"app": "mirror-pod"} - replicas := int32(2) - mirrorDeploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "echo-pods", - Namespace: "default", - }, - Spec: appsv1.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: lbls, - }, - Replicas: &replicas, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: lbls, - Annotations: map[string]string{ - "danm.k8s.io/interfaces": "[{\"tenantNetwork\":\"tenant-vxlan\", \"ip\":\"dynamic\"}]", - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "mirror-pod", - Image: "dmajrekar/nginx-echo:latest", - ImagePullPolicy: corev1.PullIfNotPresent, - Ports: []corev1.ContainerPort{ - {Protocol: "TCP", ContainerPort: 8080}, - {Protocol: "TCP", ContainerPort: 8081}, - {Protocol: "TCP", ContainerPort: 8443}, - {Protocol: "TCP", ContainerPort: 8444}, - }, - }, - }, - }, - }, - }, - } - - fmt.Println("Creating mirror deployment") - err := c.Create(context.TODO(), mirrorDeploy) - return mirrorDeploy, err - -} diff --git a/e2e/loadbalancer_test.go b/e2e/loadbalancer_test.go new file mode 100644 index 0000000..c48df7c --- /dev/null +++ b/e2e/loadbalancer_test.go @@ -0,0 +1,282 @@ +package test + +import ( + "context" + "fmt" + "testing" + + "github.com/civo/civogo" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +func TestLoadbalancerBasic(t *testing.T) { + + g := NewGomegaWithT(t) + + mirrorDeploy, err := deployMirrorPods(e2eTest.tenantClient) + g.Expect(err).ShouldNot(HaveOccurred()) + + lbls := map[string]string{"app": "mirror-pod"} + // Create a service of type: LoadBalancer + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "echo-pods", + Namespace: "default", + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Name: "http", Protocol: "TCP", Port: 80, TargetPort: intstr.FromInt(8080)}, + {Name: "https", Protocol: "TCP", Port: 443, TargetPort: intstr.FromInt(8443)}, + }, + Selector: lbls, + Type: "LoadBalancer", + }, + } + + fmt.Println("Creating Service") + err = e2eTest.tenantClient.Create(context.TODO(), svc) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Eventually(func() string { + err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) + if len(svc.Status.LoadBalancer.Ingress) == 0 { + return "" + } + return svc.Status.LoadBalancer.Ingress[0].IP + }, "2m", "5s").ShouldNot(BeEmpty()) + + // Cleanup + err = cleanUp(mirrorDeploy, svc) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Eventually(func() error { + return e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) + }, "2m", "5s").ShouldNot(BeNil()) +} + +func TestLoadbalancerProxy(t *testing.T) { + g := NewGomegaWithT(t) + + mirrorDeploy, err := deployMirrorPods(e2eTest.tenantClient) + g.Expect(err).ShouldNot(HaveOccurred()) + + lbls := map[string]string{"app": "mirror-pod"} + // Create a service of type: LoadBalancer + svc := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "echo-pods", + Namespace: "default", + Annotations: map[string]string{ + "kubernetes.civo.com/loadbalancer-enable-proxy-protocol": "send-proxy", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Name: "http", Protocol: "TCP", Port: 80, TargetPort: intstr.FromInt(8081)}, + {Name: "https", Protocol: "TCP", Port: 443, TargetPort: intstr.FromInt(8444)}, + }, + Selector: lbls, + Type: "LoadBalancer", + }, + } + + fmt.Println("Creating Service") + err = e2eTest.tenantClient.Create(context.TODO(), svc) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Eventually(func() string { + err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) + if len(svc.Status.LoadBalancer.Ingress) == 0 { + return "" + } + return svc.Status.LoadBalancer.Ingress[0].Hostname + }, "5m", "5s").ShouldNot(BeEmpty()) + + // Cleanup + err = cleanUp(mirrorDeploy, svc) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Eventually(func() error { + return e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) + }, "2m", "5s").ShouldNot(BeNil()) + +} + +func TestLoadbalancerReservedIP(t *testing.T) { + g := NewGomegaWithT(t) + + _, err := deployMirrorPods(e2eTest.tenantClient) + g.Expect(err).ShouldNot(HaveOccurred()) + + fmt.Println("Create a reserved IP for e2e test (if it doesn't exist)") + ip, err := getOrCreateIP(e2eTest.civo) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Eventually(func() string { + ip, err = e2eTest.civo.GetIP(ip.ID) + return ip.IP + }, "2m", "5s").ShouldNot(BeEmpty()) + + fmt.Println("Creating Service") + svc, err := getOrCreateSvc(e2eTest.tenantClient) + g.Expect(err).ShouldNot(HaveOccurred()) + + patchSvc := &corev1.Service{} + err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), patchSvc) + originalSvc := svc.DeepCopy() + if patchSvc.Annotations == nil { + patchSvc.Annotations = make(map[string]string, 0) + } + patchSvc.Annotations = map[string]string{ + "kubernetes.civo.com/ipv4-address": ip.IP, + } + + fmt.Println("Updating service with reserved IP annotation") + err = e2eTest.tenantClient.Patch(context.TODO(), patchSvc, client.MergeFrom(originalSvc)) + g.Expect(err).ShouldNot(HaveOccurred()) + + g.Eventually(func() string { + err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) + if len(svc.Status.LoadBalancer.Ingress) == 0 { + return "" + } + return svc.Status.LoadBalancer.Ingress[0].IP + }, "5m", "5s").Should(Equal(ip.IP)) + + // Unassign reserved IP + fmt.Println("Unassigning IP from LB") + svc.Annotations = nil + err = e2eTest.tenantClient.Update(context.TODO(), svc) + g.Expect(err).ShouldNot(HaveOccurred()) + + fmt.Println("Waiting for auto-assigned IP to be attached to LB") + g.Eventually(func() string { + err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) + if len(svc.Status.LoadBalancer.Ingress) == 0 { + return "" + } + return svc.Status.LoadBalancer.Ingress[0].IP + }, "5m", "5s").ShouldNot(Equal(ip.IP)) + + // To make sure an auto-assigned IP is actually assigned to the LB + g.Eventually(func() string { + err = e2eTest.tenantClient.Get(context.TODO(), client.ObjectKeyFromObject(svc), svc) + if len(svc.Status.LoadBalancer.Ingress) == 0 { + return "" + } + return svc.Status.LoadBalancer.Ingress[0].IP + }, "5m", "5s").ShouldNot(BeEmpty()) + +} + +func cleanUp(mirrorDeploy *appsv1.Deployment, svc *corev1.Service) error { + err := e2eTest.tenantClient.Delete(context.TODO(), svc) + if err != nil { + return err + } + + return e2eTest.tenantClient.Delete(context.TODO(), mirrorDeploy) +} + +func getOrCreateIP(c *civogo.Client) (*civogo.IP, error) { + ip, err := c.FindIP("e2e-test-ip") + if err != nil && civogo.ZeroMatchesError.Is(err) { + ip, err = c.NewIP(&civogo.CreateIPRequest{ + Name: "e2e-test-ip", + }) + if err != nil { + return nil, err + } + } else if err != nil { + return nil, err + } + return ip, err +} + +func getOrCreateSvc(c client.Client) (*corev1.Service, error) { + svc := &corev1.Service{} + err := c.Get(context.TODO(), client.ObjectKey{Name: "echo-pods", Namespace: "default"}, svc) + if err != nil && errors.IsNotFound(err) { + lbls := map[string]string{"app": "mirror-pod"} + // Create a service of type: LoadBalancer + svc = &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "echo-pods", + Namespace: "default", + Annotations: map[string]string{}, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + {Name: "http", Protocol: "TCP", Port: 80, TargetPort: intstr.FromInt(8081)}, + {Name: "https", Protocol: "TCP", Port: 443, TargetPort: intstr.FromInt(8444)}, + }, + Selector: lbls, + Type: "LoadBalancer", + }, + } + err = c.Create(context.TODO(), svc) + return svc, err + } else if err != nil { + return nil, err + } + return nil, err +} + +func deployMirrorPods(c client.Client) (*appsv1.Deployment, error) { + mirrorDeploy := &appsv1.Deployment{} + err := c.Get(context.TODO(), client.ObjectKey{Name: "echo-pods", Namespace: "default"}, mirrorDeploy) + if err != nil && errors.IsNotFound(err) { + lbls := map[string]string{"app": "mirror-pod"} + replicas := int32(2) + mirrorDeploy = &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "echo-pods", + Namespace: "default", + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: lbls, + }, + Replicas: &replicas, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: lbls, + Annotations: map[string]string{ + "danm.k8s.io/interfaces": "[{\"tenantNetwork\":\"tenant-vxlan\", \"ip\":\"dynamic\"}]", + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "mirror-pod", + Image: "dmajrekar/nginx-echo:latest", + ImagePullPolicy: corev1.PullIfNotPresent, + Ports: []corev1.ContainerPort{ + {Protocol: "TCP", ContainerPort: 8080}, + {Protocol: "TCP", ContainerPort: 8081}, + {Protocol: "TCP", ContainerPort: 8443}, + {Protocol: "TCP", ContainerPort: 8444}, + }, + }, + }, + }, + }, + }, + } + + fmt.Println("Creating mirror deployment") + err := c.Create(context.TODO(), mirrorDeploy) + return mirrorDeploy, err + } else if err != nil { + return nil, err + } + + return mirrorDeploy, err +} diff --git a/e2e/suite_test.go b/e2e/suite_test.go index c175729..93e4487 100644 --- a/e2e/suite_test.go +++ b/e2e/suite_test.go @@ -28,7 +28,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -const CivoRegion = "LON1" +var CivoRegion, CivoURL string var e2eTest *E2ETest @@ -132,18 +132,32 @@ func TestMain(m *testing.M) { } func (e *E2ETest) provisionCluster() { - api_key := os.Getenv("CIVO_API_KEY") - if api_key == "" { + APIKey := os.Getenv("CIVO_API_KEY") + if APIKey == "" { log.Panic("CIVO_API_KEY env variable not provided") } + + CivoRegion = os.Getenv("CIVO_REGION") + if CivoRegion == "" { + CivoRegion = "LON1" + } + + CivoURL := os.Getenv("CIVO_URL") + if CivoURL == "" { + CivoURL = "https://api.civo.com" + } + var err error - e.civo, err = civogo.NewClient(api_key, CivoRegion) + e.civo, err = civogo.NewClientWithURL(APIKey, CivoURL, CivoRegion) if err != nil { log.Panicf("Unable to initialise Civo Client: %s", err.Error()) } // List Clusters - list, _ := e.civo.ListKubernetesClusters() + list, err := e.civo.ListKubernetesClusters() + if err != nil { + log.Panicf("Unable to list Clusters: %s", err.Error()) + } for _, cluster := range list.Items { if cluster.Name == "ccm-e2e-test" { e.cluster = &cluster @@ -205,17 +219,17 @@ func run(secret *corev1.Secret, kubeconfig string) { } // Read env var from in cluster secret - civo.ApiURL = string(secret.Data["api-url"]) - civo.ApiKey = string(secret.Data["api-key"]) + civo.APIURL = string(secret.Data["api-url"]) + civo.APIKey = string(secret.Data["api-key"]) civo.Region = string(secret.Data["region"]) civo.ClusterID = string(secret.Data["cluster-id"]) - if civo.ApiURL == "" || civo.ApiKey == "" || civo.Region == "" || civo.ClusterID == "" { + if civo.APIURL == "" || civo.APIKey == "" || civo.Region == "" || civo.ClusterID == "" { fmt.Println("CIVO_API_URL, CIVO_API_KEY, CIVO_REGION, CIVO_CLUSTER_ID environment variables must be set") os.Exit(1) } - klog.Infof("Starting ccm with CIVO_API_URL: %s, CIVO_REGION: %s, CIVO_CLUSTER_ID: %s", civo.ApiURL, civo.Region, civo.ClusterID) + klog.Infof("Starting ccm with CIVO_API_URL: %s, CIVO_REGION: %s, CIVO_CLUSTER_ID: %s", civo.APIURL, civo.Region, civo.ClusterID) opts, err := options.NewCloudControllerManagerOptions() if err != nil { diff --git a/go.mod b/go.mod index ab24537..4f12be7 100644 --- a/go.mod +++ b/go.mod @@ -3,29 +3,20 @@ module github.com/civo/civo-cloud-controller-manager go 1.16 require ( - github.com/civo/civogo v0.2.70 + github.com/civo/civogo v0.2.93 github.com/emicklei/go-restful v2.11.1+incompatible // indirect - github.com/evanphx/json-patch v4.11.0+incompatible // indirect github.com/go-openapi/swag v0.19.7 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect - github.com/hashicorp/golang-lru v0.5.4 // indirect - github.com/imdario/mergo v0.3.12 // indirect github.com/joho/godotenv v1.4.0 - github.com/onsi/gomega v1.18.0 - github.com/prometheus/client_golang v1.11.0 // indirect + github.com/onsi/gomega v1.19.0 github.com/sirupsen/logrus v1.8.1 // indirect - go.uber.org/zap v1.17.0 // indirect - golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b // indirect - google.golang.org/appengine v1.6.7 // indirect k8s.io/api v0.21.1 k8s.io/apimachinery v0.21.1 k8s.io/client-go v12.0.0+incompatible k8s.io/cloud-provider v0.21.1 k8s.io/component-base v0.21.1 k8s.io/klog/v2 v2.9.0 - k8s.io/utils v0.0.0-20210527160623-6fdb442a123b // indirect sigs.k8s.io/controller-runtime v0.0.0-00010101000000-000000000000 ) diff --git a/go.sum b/go.sum index 582f95d..3053a3a 100644 --- a/go.sum +++ b/go.sum @@ -67,8 +67,8 @@ github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/civo/civogo v0.2.70 h1:OmZkM2dKCbJ2Gx70exPQtzdRSHElPeASHAPs4Z9bx7Y= -github.com/civo/civogo v0.2.70/go.mod h1:WUgi+GbpYlgXTbpU5Lx4scLc2XuoYhb9o20FMwPalBo= +github.com/civo/civogo v0.2.93 h1:HE6p5K9yJ9+I1IlK70aMHHoBRn6VkmMQSnUAPPM+KOo= +github.com/civo/civogo v0.2.93/go.mod h1:7+GeeFwc4AYTULaEshpT2vIcl3Qq8HPoxA17viX3l6g= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa h1:OaNxuTZr7kxeODyLWsRMC+OD03aFUH+mW6r2d+MWa5Y= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= @@ -129,6 +129,7 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v0.4.0 h1:K7/B1jt6fIBQVd4Owv2MqGQClcgf0R266+7C/QjRcLc= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/zapr v0.4.0 h1:uc1uML3hRYL9/ZZPdgHS/n8Nzo+eaYL/Efxkkamf7OM= github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= @@ -333,16 +334,16 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.0 h1:ngbYoRctxjl8SiF7XgP0NxBFbfHcg3wfHMMaFHWwMTM= -github.com/onsi/gomega v1.18.0/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= @@ -523,8 +524,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b h1:eB48h3HiRycXNy8E0Gf5e0hv7YT6Kt14L/D73G1fuwo= -golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -584,20 +585,23 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -751,6 +755,7 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.21.1 h1:94bbZ5NTjdINJEdzOkpS4vdPhkb1VFpTYC9zh43f75c= k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= +k8s.io/apiextensions-apiserver v0.21.1 h1:AA+cnsb6w7SZ1vD32Z+zdgfXdXY8X9uGX5bN6EoPEIo= k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= k8s.io/apimachinery v0.21.1 h1:Q6XuHGlj2xc+hlMCvqyYfbv3H7SRGn2c8NycxJquDVs= k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= diff --git a/vendor/github.com/civo/civogo/account.go b/vendor/github.com/civo/civogo/account.go new file mode 100644 index 0000000..d31863f --- /dev/null +++ b/vendor/github.com/civo/civogo/account.go @@ -0,0 +1,43 @@ +package civogo + +import ( + "bytes" + "encoding/json" +) + +// PaginatedAccounts returns a paginated list of Account object +type PaginatedAccounts struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Pages int `json:"pages"` + Items []Account `json:"items"` +} + +// ListAccounts lists all accounts +func (c *Client) ListAccounts() (*PaginatedAccounts, error) { + resp, err := c.SendGetRequest("/v2/accounts") + if err != nil { + return nil, decodeError(err) + } + + accounts := &PaginatedAccounts{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&accounts); err != nil { + return nil, decodeError(err) + } + + return accounts, nil +} + +// GetAccountID returns the account ID +func (c *Client) GetAccountID() string { + accounts, err := c.ListAccounts() + if err != nil { + return "" + } + + if len(accounts.Items) == 0 { + return "No account found" + } + + return accounts.Items[0].ID +} diff --git a/vendor/github.com/civo/civogo/application.go b/vendor/github.com/civo/civogo/application.go new file mode 100644 index 0000000..29b3dfe --- /dev/null +++ b/vendor/github.com/civo/civogo/application.go @@ -0,0 +1,204 @@ +package civogo + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + + "github.com/civo/civogo/utils" +) + +// Application is the struct for the Application model +type Application struct { + Name string `json:"name" validate:"required"` + ID string `json:"id"` + NetworkID string `json:"network_id" validate:"required"` + Description string `json:"description"` + Image string `json:"image"` + Size string `json:"size"` + ProcessInfo []ProcessInfo `json:"process_info,omitempty"` + Domains []string `json:"domains,omitempty"` + SSHKeyIDs []string `json:"ssh_key_ids,omitempty"` + Config []EnvVar `json:"config,omitempty"` + // Status can be one of: + // - "building": Implies platform is building + // - "available": Implies platform is available to accept image + // - "ready": Implies app is ready + Status string `json:"status"` +} + +// ApplicationConfig describes the parameters for a new CivoApp +type ApplicationConfig struct { + Name string `json:"name" validate:"required"` + NetworkID string `json:"network_id" validate:"required"` + Description string `json:"description"` + Size string `json:"size"` + SSHKeyIDs []string `json:"ssh_key_ids,omitempty"` +} + +// UpdateApplicationRequest is the struct for the UpdateApplication request +type UpdateApplicationRequest struct { + Name string `json:"name"` + Advanced bool `json:"advanced"` + Image string `json:"image" ` + Description string `json:"description"` + ProcessInfo []ProcessInfo `json:"process_info"` + Size string `json:"size" schema:"size"` + SSHKeyIDs []string `json:"ssh_key_ids" ` + Config []EnvVar `json:"config"` + Domains []string `json:"domains"` +} + +// PaginatedApplications returns a paginated list of Application object +type PaginatedApplications struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Pages int `json:"pages"` + Items []Application `json:"items"` +} + +// EnvVar holds key-value pairs for an application +type EnvVar struct { + Name string `json:"name"` + Value string `json:"value"` +} + +// ProcessInfo contains the information about the process obtained from Procfile +type ProcessInfo struct { + ProcessType string `json:"processType"` + ProcessCount int `json:"processCount"` +} + +// ErrAppDomainNotFound is returned when the domain is not found +var ErrAppDomainNotFound = fmt.Errorf("domain not found") + +// ListApplications returns all applications in that specific region +func (c *Client) ListApplications() (*PaginatedApplications, error) { + resp, err := c.SendGetRequest("/v2/applications") + if err != nil { + return nil, decodeError(err) + } + + application := &PaginatedApplications{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&application); err != nil { + return nil, decodeError(err) + } + + return application, nil +} + +// GetApplication returns an application by ID +func (c *Client) GetApplication(id string) (*Application, error) { + resp, err := c.SendGetRequest(fmt.Sprintf("/v2/applications/%s", id)) + if err != nil { + return nil, decodeError(err) + } + + application := &Application{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&application); err != nil { + return nil, decodeError(err) + } + + return application, nil +} + +// NewApplicationConfig returns an initialized config for a new application +func (c *Client) NewApplicationConfig() (*ApplicationConfig, error) { + network, err := c.GetDefaultNetwork() + if err != nil { + return nil, decodeError(err) + } + + return &ApplicationConfig{ + Name: utils.RandomName(), + NetworkID: network.ID, + Description: "", + Size: "small", + SSHKeyIDs: []string{}, + }, nil +} + +// FindApplication finds an application by either part of the ID or part of the name +func (c *Client) FindApplication(search string) (*Application, error) { + apps, err := c.ListApplications() + if err != nil { + return nil, decodeError(err) + } + + exactMatch := false + partialMatchesCount := 0 + result := Application{} + + for _, value := range apps.Items { + if value.Name == search || value.ID == search { + exactMatch = true + result = value + } else if strings.Contains(value.Name, search) || strings.Contains(value.ID, search) { + if !exactMatch { + result = value + partialMatchesCount++ + } + } + } + + if exactMatch || partialMatchesCount == 1 { + return &result, nil + } else if partialMatchesCount > 1 { + err := fmt.Errorf("unable to find %s because there were multiple matches", search) + return nil, MultipleMatchesError.wrap(err) + } else { + err := fmt.Errorf("unable to find %s, zero matches", search) + return nil, ZeroMatchesError.wrap(err) + } +} + +// CreateApplication creates a new application +func (c *Client) CreateApplication(config *ApplicationConfig) (*Application, error) { + body, err := c.SendPostRequest("/v2/applications", config) + if err != nil { + return nil, decodeError(err) + } + + var application Application + if err := json.NewDecoder(bytes.NewReader(body)).Decode(&application); err != nil { + return nil, err + } + + return &application, nil +} + +// UpdateApplication updates an application +func (c *Client) UpdateApplication(id string, application *UpdateApplicationRequest) (*Application, error) { + body, err := c.SendPutRequest(fmt.Sprintf("/v2/applications/%s", id), application) + if err != nil { + return nil, decodeError(err) + } + + updatedApplication := &Application{} + if err := json.NewDecoder(bytes.NewReader(body)).Decode(updatedApplication); err != nil { + return nil, err + } + + return updatedApplication, nil +} + +// DeleteApplication deletes an application +func (c *Client) DeleteApplication(id string) (*SimpleResponse, error) { + resp, err := c.SendDeleteRequest(fmt.Sprintf("/v2/applications/%s", id)) + if err != nil { + return nil, decodeError(err) + } + + return c.DecodeSimpleResponse(resp) +} + +// GetApplicationLogAuth returns an application log auth +func (c *Client) GetApplicationLogAuth(id string) (string, error) { + resp, err := c.SendGetRequest(fmt.Sprintf("/v2/applications/%s/log_auth", id)) + if err != nil { + return "", decodeError(err) + } + + return string(resp), nil +} diff --git a/vendor/github.com/civo/civogo/client.go b/vendor/github.com/civo/civogo/client.go index 4fad870..9a848c0 100644 --- a/vendor/github.com/civo/civogo/client.go +++ b/vendor/github.com/civo/civogo/client.go @@ -46,6 +46,19 @@ type SimpleResponse struct { ErrorDetails string `json:"details"` } +// ConfigAdvanceClientForTesting initializes a Client connecting to a local test server and allows for specifying methods +type ConfigAdvanceClientForTesting struct { + Method string + Value []ValueAdvanceClientForTesting +} + +// ValueAdvanceClientForTesting is a struct that holds the URL and the request body +type ValueAdvanceClientForTesting struct { + RequestBody string + URL string + ResponseBody string +} + // ResultSuccess represents a successful SimpleResponse const ResultSuccess = "success" @@ -86,7 +99,7 @@ func NewClient(apiKey, region string) (*Client, error) { } // NewAdvancedClientForTesting initializes a Client connecting to a local test server and allows for specifying methods -func NewAdvancedClientForTesting(responses map[string]map[string]string) (*Client, *httptest.Server, error) { +func NewAdvancedClientForTesting(responses []ConfigAdvanceClientForTesting) (*Client, *httptest.Server, error) { var responseSent bool server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { @@ -98,17 +111,23 @@ func NewAdvancedClientForTesting(responses map[string]map[string]string) (*Clien req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) - for url, criteria := range responses { - if strings.Contains(req.URL.String(), url) && - req.Method == criteria["method"] { - if criteria["method"] == "PUT" || criteria["method"] == "POST" || criteria["method"] == "PATCH" { - if strings.TrimSpace(string(body)) == strings.TrimSpace(criteria["requestBody"]) { + for _, criteria := range responses { + // we check the method first + if criteria.Method == "PUT" || criteria.Method == "POST" || criteria.Method == "PATCH" { + for _, criteria := range criteria.Value { + if req.URL.Path == criteria.URL { + if strings.TrimSpace(string(body)) == strings.TrimSpace(criteria.RequestBody) { + responseSent = true + rw.Write([]byte(criteria.ResponseBody)) + } + } + } + } else { + for _, criteria := range criteria.Value { + if req.URL.Path == criteria.URL { responseSent = true - rw.Write([]byte(criteria["responseBody"])) + rw.Write([]byte(criteria.ResponseBody)) } - } else { - responseSent = true - rw.Write([]byte(criteria["responseBody"])) } } } diff --git a/vendor/github.com/civo/civogo/errors.go b/vendor/github.com/civo/civogo/errors.go index 8ec53ee..ad43189 100644 --- a/vendor/github.com/civo/civogo/errors.go +++ b/vendor/github.com/civo/civogo/errors.go @@ -101,6 +101,11 @@ var ( DatabaseKubernetesClusterNotFoundError = constError("DatabaseKubernetesClusterNotFound") DatabaseKubernetesNodeNotFoundError = constError("DatabaseKubernetesNodeNotFound") + DatabaseClusterPoolNotFoundError = constError("DatabaseClusterPoolNotFound") + DatabaseClusterPoolInstanceNotFoundError = constError("DatabaseClusterPoolInstanceNotFound") + DatabaseClusterPoolInstanceDeleteFailedError = constError("DatabaseClusterPoolInstanceDeleteFailed") + DatabaseClusterPoolNoSufficientInstancesAvailableError = constError("DatabaseClusterPoolNoSufficientInstancesAvailable") + DatabaseListingAccountsError = constError("DatabaseListingAccountsError") DatabaseListingMembershipsError = constError("DatabaseListingMembershipsError") DatabaseMembershipCannotDeleteError = constError("DatabaseMembershipCannotDeleteError") @@ -938,6 +943,18 @@ func decodeError(err error) error { case "database_kubernetes_node_not_found": err := errors.New(msg.String()) return DatabaseKubernetesNodeNotFoundError.wrap(err) + case "database_cluster_pool_not_found": + err := errors.New(msg.String()) + return DatabaseClusterPoolNotFoundError.wrap(err) + case "database_cluster_pool_instance_not_found": + err := errors.New(msg.String()) + return DatabaseClusterPoolInstanceNotFoundError.wrap(err) + case "database_cluster_pool_instance_delete_failed": + err := errors.New(msg.String()) + return DatabaseClusterPoolInstanceDeleteFailedError.wrap(err) + case "database_cluster_pool_no_sufficient_instances_available": + err := errors.New(msg.String()) + return DatabaseClusterPoolNoSufficientInstancesAvailableError.wrap(err) case "database_instance_already_in_rescue_state": err := errors.New(msg.String()) return DatabaseInstanceAlreadyinRescueStateError.wrap(err) diff --git a/vendor/github.com/civo/civogo/fake_client.go b/vendor/github.com/civo/civogo/fake_client.go index 39eda10..7b8bdb1 100644 --- a/vendor/github.com/civo/civogo/fake_client.go +++ b/vendor/github.com/civo/civogo/fake_client.go @@ -19,6 +19,7 @@ type FakeClient struct { InstanceSizes []InstanceSize Instances []Instance Clusters []KubernetesCluster + IP []IP Networks []Network Volumes []Volume SSHKeys []SSHKey @@ -31,6 +32,7 @@ type FakeClient struct { OrganisationTeams []Team OrganisationTeamMembers map[string][]TeamMember LoadBalancers []LoadBalancer + Pools []KubernetesPool // Snapshots []Snapshot // Templates []Template } @@ -101,6 +103,13 @@ type Clienter interface { ListKubernetesClusterInstances(id string) ([]Instance, error) FindKubernetesClusterInstance(clusterID, search string) (*Instance, error) + //Pools + ListKubernetesClusterPools(cid string) ([]KubernetesPool, error) + GetKubernetesClusterPool(cid, pid string) (*KubernetesPool, error) + FindKubernetesClusterPool(cid, search string) (*KubernetesPool, error) + DeleteKubernetesClusterPoolInstance(cid, pid, id string) (*SimpleResponse, error) + UpdateKubernetesClusterPool(cid, pid string, config *KubernetesClusterPoolUpdateConfig) (*KubernetesPool, error) + // Networks GetDefaultNetwork() (*Network, error) NewNetwork(label string) (*NetworkResult, error) @@ -158,6 +167,16 @@ type Clienter interface { UpdateWebhook(id string, r *WebhookConfig) (*Webhook, error) DeleteWebhook(id string) (*SimpleResponse, error) + // Reserved IPs + ListIPs() (*PaginatedIPs, error) + FindIP(search string) (*IP, error) + GetIP(id string) (*IP, error) + NewIP(v *CreateIPRequest) (*IP, error) + UpdateIP(id string, v *UpdateIPRequest) (*IP, error) + DeleteIP(id string) (*SimpleResponse, error) + AssignIP(id, resourceID, resourceType string) (*SimpleResponse, error) + UnassignIP(id string) (*SimpleResponse, error) + // LoadBalancer ListLoadBalancers() ([]LoadBalancer, error) GetLoadBalancer(id string) (*LoadBalancer, error) @@ -1572,3 +1591,244 @@ func (c *FakeClient) DeleteLoadBalancer(id string) (*SimpleResponse, error) { return &SimpleResponse{Result: "failed"}, nil } + +// ListKubernetesClusterPools implemented in a fake way for automated tests +func (c *FakeClient) ListKubernetesClusterPools(cid string) ([]KubernetesPool, error) { + pools := []KubernetesPool{} + found := false + + for _, cs := range c.Clusters { + if cs.ID == cid { + found = true + pools = cs.Pools + break + } + } + + if found { + return pools, nil + } + + err := fmt.Errorf("unable to get kubernetes cluster %s", cid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) +} + +// GetKubernetesClusterPool implemented in a fake way for automated tests +func (c *FakeClient) GetKubernetesClusterPool(cid, pid string) (*KubernetesPool, error) { + pool := &KubernetesPool{} + clusterFound := false + poolFound := false + + for _, cs := range c.Clusters { + if cs.ID == cid { + clusterFound = true + for _, p := range cs.Pools { + if p.ID == pid { + poolFound = true + pool = &p + break + } + } + } + } + + if !clusterFound { + err := fmt.Errorf("unable to get kubernetes cluster %s", cid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + if !poolFound { + err := fmt.Errorf("unable to get kubernetes pool %s", pid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + return pool, nil +} + +// FindKubernetesClusterPool implemented in a fake way for automated tests +func (c *FakeClient) FindKubernetesClusterPool(cid, search string) (*KubernetesPool, error) { + pool := &KubernetesPool{} + clusterFound := false + poolFound := false + + for _, cs := range c.Clusters { + if cs.ID == cid { + clusterFound = true + for _, p := range cs.Pools { + if p.ID == search || strings.Contains(p.ID, search) { + poolFound = true + pool = &p + break + } + } + } + } + + if !clusterFound { + err := fmt.Errorf("unable to get kubernetes cluster %s", cid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + if !poolFound { + err := fmt.Errorf("unable to get kubernetes pool %s", search) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + return pool, nil +} + +// DeleteKubernetesClusterPoolInstance implemented in a fake way for automated tests +func (c *FakeClient) DeleteKubernetesClusterPoolInstance(cid, pid, id string) (*SimpleResponse, error) { + clusterFound := false + poolFound := false + instanceFound := false + + for ci, cs := range c.Clusters { + if cs.ID == cid { + clusterFound = true + for pi, p := range cs.Pools { + if p.ID == pid { + poolFound = true + for i, in := range p.Instances { + if in.ID == id { + instanceFound = true + p.Instances = append(p.Instances[:i], p.Instances[i+1:]...) + + instanceNames := []string{} + for _, in := range p.Instances { + instanceNames = append(instanceNames, in.Hostname) + } + p.InstanceNames = instanceNames + c.Clusters[ci].Pools[pi] = p + break + } + } + } + } + } + } + + if !clusterFound { + err := fmt.Errorf("unable to get kubernetes cluster %s", cid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + if !poolFound { + err := fmt.Errorf("unable to get kubernetes pool %s", pid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + if !instanceFound { + err := fmt.Errorf("unable to get kubernetes pool instance %s", id) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + return &SimpleResponse{ + Result: "success", + }, nil +} + +// UpdateKubernetesClusterPool implemented in a fake way for automated tests +func (c *FakeClient) UpdateKubernetesClusterPool(cid, pid string, config *KubernetesClusterPoolUpdateConfig) (*KubernetesPool, error) { + clusterFound := false + poolFound := false + + pool := KubernetesPool{} + for _, cs := range c.Clusters { + if cs.ID == cid { + clusterFound = true + for _, p := range cs.Pools { + if p.ID == pid { + poolFound = true + p.Count = config.Count + pool = p + } + } + } + } + + if !clusterFound { + err := fmt.Errorf("unable to get kubernetes cluster %s", cid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + if !poolFound { + err := fmt.Errorf("unable to get kubernetes pool %s", pid) + return nil, DatabaseKubernetesClusterNotFoundError.wrap(err) + } + + return &pool, nil +} + +// ListIPs returns a list of fake IPs +func (c *FakeClient) ListIPs() (*PaginatedIPs, error) { + return &PaginatedIPs{ + Page: 1, + PerPage: 20, + Pages: 100, + Items: []IP{ + { + ID: c.generateID(), + Name: "test-ip", + IP: c.generatePublicIP(), + }, + }, + }, nil +} + +// GetIP returns a fake IP +func (c *FakeClient) GetIP(id string) (*IP, error) { + return &IP{ + ID: c.generateID(), + Name: "test-ip", + IP: c.generatePublicIP(), + }, nil +} + +// FindIP finds a fake IP +func (c *FakeClient) FindIP(search string) (*IP, error) { + return &IP{ + ID: c.generateID(), + Name: "test-ip", + IP: c.generatePublicIP(), + }, nil +} + +// NewIP creates a fake IP +func (c *FakeClient) NewIP(v *CreateIPRequest) (*IP, error) { + return &IP{ + ID: c.generateID(), + Name: "test-ip", + IP: c.generatePublicIP(), + }, nil +} + +// UpdateIP updates a fake IP +func (c *FakeClient) UpdateIP(id string, v *UpdateIPRequest) (*IP, error) { + return &IP{ + ID: c.generateID(), + Name: v.Name, + IP: c.generatePublicIP(), + }, nil +} + +// DeleteIP deletes a fake IP +func (c *FakeClient) DeleteIP(id string) (*SimpleResponse, error) { + return &SimpleResponse{ + Result: "success", + }, nil +} + +// AssignIP assigns a fake IP +func (c *FakeClient) AssignIP(id, resourceID, resourceType string) (*SimpleResponse, error) { + return &SimpleResponse{ + Result: "success", + }, nil +} + +// UnassignIP unassigns a fake IP +func (c *FakeClient) UnassignIP(id string) (*SimpleResponse, error) { + return &SimpleResponse{ + Result: "success", + }, nil +} diff --git a/vendor/github.com/civo/civogo/go.mod b/vendor/github.com/civo/civogo/go.mod index 6d62383..80f9809 100644 --- a/vendor/github.com/civo/civogo/go.mod +++ b/vendor/github.com/civo/civogo/go.mod @@ -2,4 +2,4 @@ module github.com/civo/civogo go 1.16 -require github.com/onsi/gomega v1.18.0 +require github.com/onsi/gomega v1.19.0 diff --git a/vendor/github.com/civo/civogo/go.sum b/vendor/github.com/civo/civogo/go.sum index 1d23045..842587f 100644 --- a/vendor/github.com/civo/civogo/go.sum +++ b/vendor/github.com/civo/civogo/go.sum @@ -28,13 +28,13 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.0 h1:ngbYoRctxjl8SiF7XgP0NxBFbfHcg3wfHMMaFHWwMTM= -github.com/onsi/gomega v1.18.0/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= +github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= @@ -48,8 +48,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -65,13 +66,16 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/vendor/github.com/civo/civogo/instance.go b/vendor/github.com/civo/civogo/instance.go index 524bfeb..68332e5 100644 --- a/vendor/github.com/civo/civogo/instance.go +++ b/vendor/github.com/civo/civogo/instance.go @@ -29,6 +29,7 @@ type Instance struct { InitialUser string `json:"initial_user,omitempty"` InitialPassword string `json:"initial_password,omitempty"` SSHKey string `json:"ssh_key,omitempty"` + SSHKeyID string `json:"ssh_key_id,omitempty"` Status string `json:"status,omitempty"` Notes string `json:"notes,omitempty"` FirewallID string `json:"firewall_id,omitempty"` @@ -45,6 +46,8 @@ type Instance struct { DiskGigabytes int `json:"disk_gb,omitempty"` Script string `json:"script,omitempty"` CreatedAt time.Time `json:"created_at,omitempty"` + ReservedIPID string `json:"reserved_ip_id,omitempty"` + ReservedIPName string `json:"reserved_ip_name,omitempty"` } //"cpu_cores":1,"ram_mb":2048,"disk_gb":25 @@ -165,7 +168,7 @@ func (c *Client) NewInstanceConfig() (*InstanceConfig, error) { return nil, decodeError(err) } - diskimage, err := c.GetDiskImageByName("ubuntu-bionic") + diskimage, err := c.GetDiskImageByName("ubuntu-focal") if err != nil { return nil, decodeError(err) } @@ -174,7 +177,7 @@ func (c *Client) NewInstanceConfig() (*InstanceConfig, error) { Count: 1, Hostname: utils.RandomName(), ReverseDNS: "", - Size: "g2.xsmall", + Size: "g3.medium", Region: c.Region, PublicIPRequired: "true", NetworkID: network.ID, diff --git a/vendor/github.com/civo/civogo/ip.go b/vendor/github.com/civo/civogo/ip.go new file mode 100644 index 0000000..1c9cd67 --- /dev/null +++ b/vendor/github.com/civo/civogo/ip.go @@ -0,0 +1,193 @@ +package civogo + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +// IP represents a serialized structure +type IP struct { + ID string `json:"id"` + Name string `json:"name,omitempty"` + IP string `json:"ip,omitempty"` + AssignedTo AssignedTo `json:"assigned_to,omitempty"` +} + +// AssignedTo represents IP assigned to resource +type AssignedTo struct { + ID string `json:"id"` + // Type can be one of the following: + // - instance + // - loadbalancer + Type string `json:"type"` + Name string `json:"name"` +} + +// CreateIPRequest is a struct for creating an IP +type CreateIPRequest struct { + // Name is an optional parameter. If not provided, name will be the IP address + Name string `json:"name,omitempty"` +} + +// PaginatedIPs is a paginated list of IPs +type PaginatedIPs struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Pages int `json:"pages"` + Items []IP `json:"items"` +} + +// UpdateIPRequest is a struct for creating an IP +type UpdateIPRequest struct { + Name string `json:"name" validate:"required"` +} + +// Actions for IP +type Actions struct { + // Action is one of "assign", "unassign" + Action string `json:"action"` + AssignToID string `json:"assign_to_id"` + AssignToType string `json:"assign_to_type"` +} + +// ListIPs returns all reserved IPs in that specific region +func (c *Client) ListIPs() (*PaginatedIPs, error) { + resp, err := c.SendGetRequest("/v2/ips") + if err != nil { + return nil, decodeError(err) + } + + ips := &PaginatedIPs{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&ips); err != nil { + return nil, err + } + + return ips, nil +} + +// GetIP finds an reserved IP by the full ID +func (c *Client) GetIP(id string) (*IP, error) { + resp, err := c.SendGetRequest(fmt.Sprintf("/v2/ips/%s", id)) + if err != nil { + return nil, decodeError(err) + } + + var ip = IP{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&ip); err != nil { + return nil, err + } + + return &ip, nil +} + +// FindIP finds an reserved IP by name or by IP +func (c *Client) FindIP(search string) (*IP, error) { + ips, err := c.ListIPs() + if err != nil { + return nil, decodeError(err) + } + + exactMatch := false + partialMatchesCount := 0 + result := IP{} + + for _, value := range ips.Items { + if value.IP == search || value.Name == search || value.ID == search { + exactMatch = true + result = value + } else if strings.Contains(value.IP, search) || strings.Contains(value.Name, search) || strings.Contains(value.ID, search) { + if !exactMatch { + result = value + partialMatchesCount++ + } + } + } + + if exactMatch || partialMatchesCount == 1 { + return &result, nil + } else if partialMatchesCount > 1 { + err := fmt.Errorf("unable to find %s because there were multiple matches", search) + return nil, MultipleMatchesError.wrap(err) + } else { + err := fmt.Errorf("unable to find %s, zero matches", search) + return nil, ZeroMatchesError.wrap(err) + } +} + +// NewIP creates a new IP +func (c *Client) NewIP(v *CreateIPRequest) (*IP, error) { + body, err := c.SendPostRequest("/v2/ips", v) + if err != nil { + return nil, decodeError(err) + } + + var result = &IP{} + if err := json.NewDecoder(bytes.NewReader(body)).Decode(result); err != nil { + return nil, err + } + + return result, nil +} + +// UpdateIP updates an IP +func (c *Client) UpdateIP(id string, v *UpdateIPRequest) (*IP, error) { + resp, err := c.SendPutRequest(fmt.Sprintf("/v2/ips/%s", id), v) + if err != nil { + return nil, decodeError(err) + } + + var result = &IP{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(result); err != nil { + return nil, err + } + + return result, nil +} + +// AssignIP assigns a reserved IP to a Civo resource +func (c *Client) AssignIP(id, resourceID, resourceType string) (*SimpleResponse, error) { + actions := &Actions{ + Action: "assign", + } + + if resourceID == "" || resourceType == "" { + return nil, fmt.Errorf("resource ID and type are required") + } + + actions.AssignToID = resourceID + actions.AssignToType = resourceType + + resp, err := c.SendPostRequest(fmt.Sprintf("/v2/ips/%s/actions", id), actions) + if err != nil { + return nil, decodeError(err) + } + + return c.DecodeSimpleResponse(resp) +} + +// UnassignIP unassigns a reserved IP from a Civo resource +// UnassignIP is an idempotent operation. If you unassign on a unassigned IP, it will return a 200 OK. +func (c *Client) UnassignIP(id string) (*SimpleResponse, error) { + actions := &Actions{ + Action: "unassign", + } + + resp, err := c.SendPostRequest(fmt.Sprintf("/v2/ips/%s/actions", id), actions) + if err != nil { + return nil, decodeError(err) + } + + return c.DecodeSimpleResponse(resp) +} + +// DeleteIP deletes an IP +func (c *Client) DeleteIP(id string) (*SimpleResponse, error) { + resp, err := c.SendDeleteRequest(fmt.Sprintf("/v2/ips/%s", id)) + if err != nil { + return nil, decodeError(err) + } + + return c.DecodeSimpleResponse(resp) +} diff --git a/vendor/github.com/civo/civogo/kubernetes.go b/vendor/github.com/civo/civogo/kubernetes.go index b070634..90e498e 100644 --- a/vendor/github.com/civo/civogo/kubernetes.go +++ b/vendor/github.com/civo/civogo/kubernetes.go @@ -84,9 +84,18 @@ type KubernetesCluster struct { CreatedAt time.Time `json:"created_at,omitempty"` Instances []KubernetesInstance `json:"instances,omitempty"` Pools []KubernetesPool `json:"pools,omitempty"` + RequiredPools []RequiredPools `json:"required_pools,omitempty"` InstalledApplications []KubernetesInstalledApplication `json:"installed_applications,omitempty"` FirewallID string `json:"firewall_id,omitempty"` CNIPlugin string `json:"cni_plugin,omitempty"` + CCMInstalled string `json:"ccm_installed,omitempty"` +} + +// RequiredPools returns the required pools for a given Kubernetes cluster +type RequiredPools struct { + ID string `json:"id"` + Size string `json:"size"` + Count int `json:"count"` } // PaginatedKubernetesClusters is a Kubernetes k3s cluster diff --git a/vendor/github.com/civo/civogo/objectstore.go b/vendor/github.com/civo/civogo/objectstore.go new file mode 100644 index 0000000..66c271b --- /dev/null +++ b/vendor/github.com/civo/civogo/objectstore.go @@ -0,0 +1,151 @@ +package civogo + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +// ObjectStore is the struct for the ObjectStore model +type ObjectStore struct { + ID string `json:"id"` + Name string `json:"name"` + GeneratedName string `json:"generated_name"` + MaxObjects int `json:"max_objects"` + MaxSize string `json:"max_size"` + AccessKeyID string `json:"access_key_id"` + SecretAccessKey string `json:"secret_access_key"` + ObjectStoreEndpoint string `json:"objectstore_endpoint"` + Status string `json:"status"` +} + +// PaginatedObjectstores is a paginated list of Objectstores +type PaginatedObjectstores struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Pages int `json:"pages"` + Items []ObjectStore `json:"items"` +} + +// CreateObjectStoreRequest holds the request to create a new object storage +type CreateObjectStoreRequest struct { + Name string `json:"name,omitempty"` + MaxSizeGB int `json:"max_size_gb" validate:"required"` + Prefix string `json:"prefix,omitempty"` + AccessKeyID string `json:"access_key_id,omitempty"` + SecretAccessKey string `json:"secret_access_key,omitempty"` + Region string `json:"region"` +} + +// UpdateObjectStoreRequest holds the request to update a specified object storage's details +type UpdateObjectStoreRequest struct { + MaxSizeGB int `json:"max_size_gb"` + AccessKeyID string `json:"access_key_id,omitempty"` + SecretAccessKey string `json:"secret_access_key,omitempty"` + Region string `json:"region,omitempty"` +} + +// ListObjectStores returns all objectstores in that specific region +func (c *Client) ListObjectStores() (*PaginatedObjectstores, error) { + resp, err := c.SendGetRequest("/v2/objectstores") + if err != nil { + return nil, decodeError(err) + } + + stores := &PaginatedObjectstores{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&stores); err != nil { + return nil, err + } + + return stores, nil +} + +// GetObjectStore finds an objectstore by the full ID +func (c *Client) GetObjectStore(id string) (*ObjectStore, error) { + resp, err := c.SendGetRequest(fmt.Sprintf("/v2/objectstores/%s", id)) + if err != nil { + return nil, decodeError(err) + } + + var os = ObjectStore{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&os); err != nil { + return nil, err + } + + return &os, nil +} + +// FindObjectStore finds an objectstore by name or by accesskeyID +func (c *Client) FindObjectStore(search string) (*ObjectStore, error) { + objectstores, err := c.ListObjectStores() + if err != nil { + return nil, decodeError(err) + } + + exactMatch := false + partialMatchesCount := 0 + result := ObjectStore{} + + for _, value := range objectstores.Items { + if value.AccessKeyID == search || value.Name == search || value.ID == search { + exactMatch = true + result = value + } else if strings.Contains(value.AccessKeyID, search) || strings.Contains(value.Name, search) || strings.Contains(value.ID, search) { + if !exactMatch { + result = value + partialMatchesCount++ + } + } + } + + if exactMatch || partialMatchesCount == 1 { + return &result, nil + } else if partialMatchesCount > 1 { + err := fmt.Errorf("unable to find %s because there were multiple matches", search) + return nil, MultipleMatchesError.wrap(err) + } else { + err := fmt.Errorf("unable to find %s, zero matches", search) + return nil, ZeroMatchesError.wrap(err) + } +} + +// NewObjectStore creates a new objectstore +func (c *Client) NewObjectStore(v *CreateObjectStoreRequest) (*ObjectStore, error) { + body, err := c.SendPostRequest("/v2/objectstores", v) + if err != nil { + return nil, decodeError(err) + } + + var result = &ObjectStore{} + if err := json.NewDecoder(bytes.NewReader(body)).Decode(result); err != nil { + return nil, err + } + + return result, nil +} + +// UpdateObjectStore updates an objectstore +func (c *Client) UpdateObjectStore(id string, v *UpdateObjectStoreRequest) (*ObjectStore, error) { + resp, err := c.SendPutRequest(fmt.Sprintf("/v2/objectstores/%s", id), v) + if err != nil { + return nil, decodeError(err) + } + + var result = &ObjectStore{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(result); err != nil { + return nil, err + } + + return result, nil +} + +// DeleteObjectStore deletes an objectstore +func (c *Client) DeleteObjectStore(id string) (*SimpleResponse, error) { + resp, err := c.SendDeleteRequest(fmt.Sprintf("/v2/objectstores/%s", id)) + if err != nil { + return nil, decodeError(err) + } + + return c.DecodeSimpleResponse(resp) +} diff --git a/vendor/github.com/civo/civogo/pool.go b/vendor/github.com/civo/civogo/pool.go new file mode 100644 index 0000000..c8000c3 --- /dev/null +++ b/vendor/github.com/civo/civogo/pool.go @@ -0,0 +1,105 @@ +package civogo + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" +) + +//KubernetesClusterPoolUpdateConfig is used to create a new cluster pool +type KubernetesClusterPoolUpdateConfig struct { + ID string `json:"id,omitempty"` + Count int `json:"count,omitempty"` + Size string `json:"size,omitempty"` + Region string `json:"region,omitempty"` +} + +// ListKubernetesClusterPools returns all the pools for a kubernetes cluster +func (c *Client) ListKubernetesClusterPools(cid string) ([]KubernetesPool, error) { + resp, err := c.SendGetRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s/pools", cid)) + if err != nil { + return nil, decodeError(err) + } + + pools := make([]KubernetesPool, 0) + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&pools); err != nil { + return nil, decodeError(err) + } + + return pools, nil +} + +// GetKubernetesClusterPool returns a pool for a kubernetes cluster +func (c *Client) GetKubernetesClusterPool(cid, pid string) (*KubernetesPool, error) { + resp, err := c.SendGetRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s/pools/%s", cid, pid)) + if err != nil { + return nil, decodeError(err) + } + + pool := &KubernetesPool{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&pool); err != nil { + return nil, decodeError(err) + } + + return pool, nil +} + +// FindKubernetesClusterPool finds a pool by either part of the ID +func (c *Client) FindKubernetesClusterPool(cid, search string) (*KubernetesPool, error) { + pools, err := c.ListKubernetesClusterPools(cid) + if err != nil { + return nil, decodeError(err) + } + + exactMatch := false + partialMatchesCount := 0 + result := KubernetesPool{} + + for _, value := range pools { + if value.ID == search { + exactMatch = true + result = value + } else if strings.Contains(value.ID, search) { + if !exactMatch { + result = value + partialMatchesCount++ + } + } + } + + if exactMatch || partialMatchesCount == 1 { + return &result, nil + } else if partialMatchesCount > 1 { + err := fmt.Errorf("unable to find %s because there were multiple matches", search) + return nil, MultipleMatchesError.wrap(err) + } else { + err := fmt.Errorf("unable to find %s, zero matches", search) + return nil, ZeroMatchesError.wrap(err) + } +} + +// DeleteKubernetesClusterPoolInstance deletes a instance from pool +func (c *Client) DeleteKubernetesClusterPoolInstance(cid, pid, id string) (*SimpleResponse, error) { + resp, err := c.SendDeleteRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s/pools/%s/instances/%s", cid, pid, id)) + if err != nil { + return nil, decodeError(err) + } + + return c.DecodeSimpleResponse(resp) +} + +// UpdateKubernetesClusterPool updates a pool for a kubernetes cluster +func (c *Client) UpdateKubernetesClusterPool(cid, pid string, config *KubernetesClusterPoolUpdateConfig) (*KubernetesPool, error) { + resp, err := c.SendPutRequest(fmt.Sprintf("/v2/kubernetes/clusters/%s/pools/%s", cid, pid), config) + if err != nil { + return nil, decodeError(err) + } + + pool := &KubernetesPool{} + if err := json.NewDecoder(bytes.NewReader(resp)).Decode(&pool); err != nil { + return nil, decodeError(err) + } + + return pool, nil +} diff --git a/vendor/github.com/onsi/gomega/CHANGELOG.md b/vendor/github.com/onsi/gomega/CHANGELOG.md index 78ca39a..4375bbc 100644 --- a/vendor/github.com/onsi/gomega/CHANGELOG.md +++ b/vendor/github.com/onsi/gomega/CHANGELOG.md @@ -1,3 +1,24 @@ +## 1.19.0 + +## Features +- New [`HaveEach`](https://onsi.github.io/gomega/#haveeachelement-interface) matcher to ensure that each and every element in an `array`, `slice`, or `map` satisfies the passed in matcher. (#523) [9fc2ae2] (#524) [c8ba582] +- Users can now wrap the `Gomega` interface to implement custom behavior on each assertion. (#521) [1f2e714] +- [`ContainElement`](https://onsi.github.io/gomega/#containelementelement-interface) now accepts an additional pointer argument. Elements that satisfy the matcher are stored in the pointer enabling developers to easily add subsequent, more detailed, assertions against the matching element. (#527) [1a4e27f] + +## Fixes +- update RELEASING instructions to match ginkgo [0917cde] +- Bump github.com/onsi/ginkgo/v2 from 2.0.0 to 2.1.3 (#519) [49ab4b0] +- Fix CVE-2021-38561 (#534) [f1b4456] +- Fix max number of samples in experiments on non-64-bit systems. (#528) [1c84497] +- Remove dependency on ginkgo v1.16.4 (#530) [4dea8d5] +- Fix for Go 1.18 (#532) [56d2a29] +- Document precendence of timeouts (#533) [b607941] + +## 1.18.1 + +## Fixes +- Add pointer support to HaveField matcher (#495) [79e41a3] + ## 1.18.0 ## Features diff --git a/vendor/github.com/onsi/gomega/RELEASING.md b/vendor/github.com/onsi/gomega/RELEASING.md index 998d64e..2d30d99 100644 --- a/vendor/github.com/onsi/gomega/RELEASING.md +++ b/vendor/github.com/onsi/gomega/RELEASING.md @@ -7,6 +7,11 @@ A Gomega release is a tagged sha and a GitHub release. To cut a release: - New Features (minor version) - Fixes (fix version) - Maintenance (which in general should not be mentioned in `CHANGELOG.md` as they have no user impact) -2. Update GOMEGA_VERSION in `gomega_dsl.go` -3. Push a commit with the version number as the commit message (e.g. `v1.3.0`) -4. Create a new [GitHub release](https://help.github.com/articles/creating-releases/) with the version number as the tag (e.g. `v1.3.0`). List the key changes in the release notes. +1. Update GOMEGA_VERSION in `gomega_dsl.go` +1. Commit, push, and release: + ``` + git commit -m "vM.m.p" + git push + gh release create "vM.m.p" + git fetch --tags origin master + ``` \ No newline at end of file diff --git a/vendor/github.com/onsi/gomega/go.mod b/vendor/github.com/onsi/gomega/go.mod index e4ff19d..276ffb1 100644 --- a/vendor/github.com/onsi/gomega/go.mod +++ b/vendor/github.com/onsi/gomega/go.mod @@ -1,11 +1,16 @@ module github.com/onsi/gomega -go 1.16 +go 1.18 require ( github.com/golang/protobuf v1.5.2 - github.com/onsi/ginkgo/v2 v2.0.0 - golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + github.com/onsi/ginkgo/v2 v2.1.3 + golang.org/x/net v0.0.0-20220225172249-27dd8689420f gopkg.in/yaml.v2 v2.4.0 ) + +require ( + golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/protobuf v1.26.0 // indirect +) diff --git a/vendor/github.com/onsi/gomega/go.sum b/vendor/github.com/onsi/gomega/go.sum index 261fa56..256e916 100644 --- a/vendor/github.com/onsi/gomega/go.sum +++ b/vendor/github.com/onsi/gomega/go.sum @@ -28,10 +28,9 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -48,8 +47,9 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -70,8 +70,9 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= diff --git a/vendor/github.com/onsi/gomega/gomega_dsl.go b/vendor/github.com/onsi/gomega/gomega_dsl.go index 81181dc..dcb7e88 100644 --- a/vendor/github.com/onsi/gomega/gomega_dsl.go +++ b/vendor/github.com/onsi/gomega/gomega_dsl.go @@ -22,7 +22,7 @@ import ( "github.com/onsi/gomega/types" ) -const GOMEGA_VERSION = "1.18.0" +const GOMEGA_VERSION = "1.19.0" const nilGomegaPanic = `You are trying to make an assertion, but haven't registered Gomega's fail handler. If you're using Ginkgo then you probably forgot to put your assertion in an It(). @@ -52,7 +52,7 @@ var Default = Gomega(internal.NewGomega(internal.FetchDefaultDurationBundle())) // rich ecosystem of matchers without causing a test to fail. For example, to aggregate a series of potential failures // or for use in a non-test setting. func NewGomega(fail types.GomegaFailHandler) Gomega { - return internal.NewGomega(Default.(*internal.Gomega).DurationBundle).ConfigureWithFailHandler(fail) + return internal.NewGomega(internalGomega(Default).DurationBundle).ConfigureWithFailHandler(fail) } // WithT wraps a *testing.T and provides `Expect`, `Eventually`, and `Consistently` methods. This allows you to leverage @@ -69,6 +69,20 @@ type WithT = internal.Gomega // GomegaWithT is deprecated in favor of gomega.WithT, which does not stutter. type GomegaWithT = WithT +// inner is an interface that allows users to provide a wrapper around Default. The wrapper +// must implement the inner interface and return either the original Default or the result of +// a call to NewGomega(). +type inner interface { + Inner() Gomega +} + +func internalGomega(g Gomega) *internal.Gomega { + if v, ok := g.(inner); ok { + return v.Inner().(*internal.Gomega) + } + return g.(*internal.Gomega) +} + // NewWithT takes a *testing.T and returngs a `gomega.WithT` allowing you to use `Expect`, `Eventually`, and `Consistently` along with // Gomega's rich ecosystem of matchers in standard `testing` test suits. // @@ -79,7 +93,7 @@ type GomegaWithT = WithT // g.Expect(f.HasCow()).To(BeTrue(), "Farm should have cow") // } func NewWithT(t types.GomegaTestingT) *WithT { - return internal.NewGomega(Default.(*internal.Gomega).DurationBundle).ConfigureWithT(t) + return internal.NewGomega(internalGomega(Default).DurationBundle).ConfigureWithT(t) } // NewGomegaWithT is deprecated in favor of gomega.NewWithT, which does not stutter. @@ -88,20 +102,20 @@ var NewGomegaWithT = NewWithT // RegisterFailHandler connects Ginkgo to Gomega. When a matcher fails // the fail handler passed into RegisterFailHandler is called. func RegisterFailHandler(fail types.GomegaFailHandler) { - Default.(*internal.Gomega).ConfigureWithFailHandler(fail) + internalGomega(Default).ConfigureWithFailHandler(fail) } // RegisterFailHandlerWithT is deprecated and will be removed in a future release. // users should use RegisterFailHandler, or RegisterTestingT func RegisterFailHandlerWithT(_ types.GomegaTestingT, fail types.GomegaFailHandler) { fmt.Println("RegisterFailHandlerWithT is deprecated. Please use RegisterFailHandler or RegisterTestingT instead.") - Default.(*internal.Gomega).ConfigureWithFailHandler(fail) + internalGomega(Default).ConfigureWithFailHandler(fail) } // RegisterTestingT connects Gomega to Golang's XUnit style // Testing.T tests. It is now deprecated and you should use NewWithT() instead to get a fresh instance of Gomega for each test. func RegisterTestingT(t types.GomegaTestingT) { - Default.(*internal.Gomega).ConfigureWithT(t) + internalGomega(Default).ConfigureWithT(t) } // InterceptGomegaFailures runs a given callback and returns an array of @@ -112,13 +126,13 @@ func RegisterTestingT(t types.GomegaTestingT) { // This is most useful when testing custom matchers, but can also be used to check // on a value using a Gomega assertion without causing a test failure. func InterceptGomegaFailures(f func()) []string { - originalHandler := Default.(*internal.Gomega).Fail + originalHandler := internalGomega(Default).Fail failures := []string{} - Default.(*internal.Gomega).Fail = func(message string, callerSkip ...int) { + internalGomega(Default).Fail = func(message string, callerSkip ...int) { failures = append(failures, message) } defer func() { - Default.(*internal.Gomega).Fail = originalHandler + internalGomega(Default).Fail = originalHandler }() f() return failures @@ -131,14 +145,14 @@ func InterceptGomegaFailures(f func()) []string { // does not register a failure with the FailHandler registered via RegisterFailHandler - it is up // to the user to decide what to do with the returned error func InterceptGomegaFailure(f func()) (err error) { - originalHandler := Default.(*internal.Gomega).Fail - Default.(*internal.Gomega).Fail = func(message string, callerSkip ...int) { + originalHandler := internalGomega(Default).Fail + internalGomega(Default).Fail = func(message string, callerSkip ...int) { err = errors.New(message) panic("stop execution") } defer func() { - Default.(*internal.Gomega).Fail = originalHandler + internalGomega(Default).Fail = originalHandler if e := recover(); e != nil { if err == nil { panic(e) @@ -151,7 +165,7 @@ func InterceptGomegaFailure(f func()) (err error) { } func ensureDefaultGomegaIsConfigured() { - if !Default.(*internal.Gomega).IsConfigured() { + if !internalGomega(Default).IsConfigured() { panic(nilGomegaPanic) } } diff --git a/vendor/github.com/onsi/gomega/matchers.go b/vendor/github.com/onsi/gomega/matchers.go index b46e461..b58dd67 100644 --- a/vendor/github.com/onsi/gomega/matchers.go +++ b/vendor/github.com/onsi/gomega/matchers.go @@ -256,16 +256,26 @@ func BeZero() types.GomegaMatcher { return &matchers.BeZeroMatcher{} } -//ContainElement succeeds if actual contains the passed in element. -//By default ContainElement() uses Equal() to perform the match, however a -//matcher can be passed in instead: +//ContainElement succeeds if actual contains the passed in element. By default +//ContainElement() uses Equal() to perform the match, however a matcher can be +//passed in instead: // Expect([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubstring("Bar"))) // -//Actual must be an array, slice or map. -//For maps, ContainElement searches through the map's values. -func ContainElement(element interface{}) types.GomegaMatcher { +//Actual must be an array, slice or map. For maps, ContainElement searches +//through the map's values. +// +//If you want to have a copy of the matching element(s) found you can pass a +//pointer to a variable of the appropriate type. If the variable isn't a slice +//or map, then exactly one match will be expected and returned. If the variable +//is a slice or map, then at least one match is expected and all matches will be +//stored in the variable. +// +// var findings []string +// Expect([]string{"Foo", "FooBar"}).Should(ContainElement(ContainSubString("Bar", &findings))) +func ContainElement(element interface{}, result ...interface{}) types.GomegaMatcher { return &matchers.ContainElementMatcher{ Element: element, + Result: result, } } @@ -320,6 +330,20 @@ func ContainElements(elements ...interface{}) types.GomegaMatcher { } } +//HaveEach succeeds if actual solely contains elements that match the passed in element. +//Please note that if actual is empty, HaveEach always will succeed. +//By default HaveEach() uses Equal() to perform the match, however a +//matcher can be passed in instead: +// Expect([]string{"Foo", "FooBar"}).Should(HaveEach(ContainSubstring("Foo"))) +// +//Actual must be an array, slice or map. +//For maps, HaveEach searches through the map's values. +func HaveEach(element interface{}) types.GomegaMatcher { + return &matchers.HaveEachMatcher{ + Element: element, + } +} + //HaveKey succeeds if actual is a map with the passed in key. //By default HaveKey uses Equal() to perform the match, however a //matcher can be passed in instead: diff --git a/vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go b/vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go index 8d6c44c..3d45c9e 100644 --- a/vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go +++ b/vendor/github.com/onsi/gomega/matchers/contain_element_matcher.go @@ -3,6 +3,7 @@ package matchers import ( + "errors" "fmt" "reflect" @@ -11,6 +12,7 @@ import ( type ContainElementMatcher struct { Element interface{} + Result []interface{} } func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, err error) { @@ -18,6 +20,49 @@ func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, e return false, fmt.Errorf("ContainElement matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) } + var actualT reflect.Type + var result reflect.Value + switch l := len(matcher.Result); { + case l > 1: + return false, errors.New("ContainElement matcher expects at most a single optional pointer to store its findings at") + case l == 1: + if reflect.ValueOf(matcher.Result[0]).Kind() != reflect.Ptr { + return false, fmt.Errorf("ContainElement matcher expects a non-nil pointer to store its findings at. Got\n%s", + format.Object(matcher.Result[0], 1)) + } + actualT = reflect.TypeOf(actual) + resultReference := matcher.Result[0] + result = reflect.ValueOf(resultReference).Elem() // what ResultReference points to, to stash away our findings + switch result.Kind() { + case reflect.Array: + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + reflect.SliceOf(actualT.Elem()).String(), result.Type().String()) + case reflect.Slice: + if !isArrayOrSlice(actual) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + reflect.MapOf(actualT.Key(), actualT.Elem()).String(), result.Type().String()) + } + if !actualT.Elem().AssignableTo(result.Type().Elem()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + case reflect.Map: + if !isMap(actual) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + if !actualT.AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + default: + if !actualT.Elem().AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.Elem().String(), result.Type().String()) + } + } + } + elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher) if !elementIsMatcher { elemMatcher = &EqualMatcher{Expected: matcher.Element} @@ -25,30 +70,99 @@ func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, e value := reflect.ValueOf(actual) var valueAt func(int) interface{} + + var getFindings func() reflect.Value + var foundAt func(int) + if isMap(actual) { keys := value.MapKeys() valueAt = func(i int) interface{} { return value.MapIndex(keys[i]).Interface() } + if result.Kind() != reflect.Invalid { + fm := reflect.MakeMap(actualT) + getFindings = func() reflect.Value { + return fm + } + foundAt = func(i int) { + fm.SetMapIndex(keys[i], value.MapIndex(keys[i])) + } + } } else { valueAt = func(i int) interface{} { return value.Index(i).Interface() } + if result.Kind() != reflect.Invalid { + var f reflect.Value + if result.Kind() == reflect.Slice { + f = reflect.MakeSlice(result.Type(), 0, 0) + } else { + f = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0) + } + getFindings = func() reflect.Value { + return f + } + foundAt = func(i int) { + f = reflect.Append(f, value.Index(i)) + } + } } var lastError error for i := 0; i < value.Len(); i++ { - success, err := elemMatcher.Match(valueAt(i)) + elem := valueAt(i) + success, err := elemMatcher.Match(elem) if err != nil { lastError = err continue } if success { - return true, nil + if result.Kind() == reflect.Invalid { + return true, nil + } + foundAt(i) } } - return false, lastError + // when the expectation isn't interested in the findings except for success + // or non-success, then we're done here and return the last matcher error + // seen, if any, as well as non-success. + if result.Kind() == reflect.Invalid { + return false, lastError + } + + // pick up any findings the test is interested in as it specified a non-nil + // result reference. However, the expection always is that there are at + // least one or multiple findings. So, if a result is expected, but we had + // no findings, then this is an error. + findings := getFindings() + if findings.Len() == 0 { + return false, lastError + } + + // there's just a single finding and the result is neither a slice nor a map + // (so it's a scalar): pick the one and only finding and return it in the + // place the reference points to. + if findings.Len() == 1 && !isArrayOrSlice(result.Interface()) && !isMap(result.Interface()) { + if isMap(actual) { + miter := findings.MapRange() + miter.Next() + result.Set(miter.Value()) + } else { + result.Set(findings.Index(0)) + } + return true, nil + } + + // at least one or even multiple findings and a the result references a + // slice or a map, so all we need to do is to store our findings where the + // reference points to. + if !findings.Type().AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return multiple findings. Need *%s, got *%s", + findings.Type().String(), result.Type().String()) + } + result.Set(findings) + return true, nil } func (matcher *ContainElementMatcher) FailureMessage(actual interface{}) (message string) { diff --git a/vendor/github.com/onsi/gomega/matchers/have_each_matcher.go b/vendor/github.com/onsi/gomega/matchers/have_each_matcher.go new file mode 100644 index 0000000..025b6e1 --- /dev/null +++ b/vendor/github.com/onsi/gomega/matchers/have_each_matcher.go @@ -0,0 +1,65 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type HaveEachMatcher struct { + Element interface{} +} + +func (matcher *HaveEachMatcher) Match(actual interface{}) (success bool, err error) { + if !isArrayOrSlice(actual) && !isMap(actual) { + return false, fmt.Errorf("HaveEach matcher expects an array/slice/map. Got:\n%s", + format.Object(actual, 1)) + } + + elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher) + if !elementIsMatcher { + elemMatcher = &EqualMatcher{Expected: matcher.Element} + } + + value := reflect.ValueOf(actual) + if value.Len() == 0 { + return false, fmt.Errorf("HaveEach matcher expects a non-empty array/slice/map. Got:\n%s", + format.Object(actual, 1)) + } + + var valueAt func(int) interface{} + if isMap(actual) { + keys := value.MapKeys() + valueAt = func(i int) interface{} { + return value.MapIndex(keys[i]).Interface() + } + } else { + valueAt = func(i int) interface{} { + return value.Index(i).Interface() + } + } + + // if there are no elements, then HaveEach will match. + for i := 0; i < value.Len(); i++ { + success, err := elemMatcher.Match(valueAt(i)) + if err != nil { + return false, err + } + if !success { + return false, nil + } + } + + return true, nil +} + +// FailureMessage returns a suitable failure message. +func (matcher *HaveEachMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to contain element matching", matcher.Element) +} + +// NegatedFailureMessage returns a suitable negated failure message. +func (matcher *HaveEachMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain element matching", matcher.Element) +} diff --git a/vendor/github.com/onsi/gomega/matchers/have_field.go b/vendor/github.com/onsi/gomega/matchers/have_field.go index 2f1a916..e1fe934 100644 --- a/vendor/github.com/onsi/gomega/matchers/have_field.go +++ b/vendor/github.com/onsi/gomega/matchers/have_field.go @@ -12,6 +12,13 @@ func extractField(actual interface{}, field string) (interface{}, error) { fields := strings.SplitN(field, ".", 2) actualValue := reflect.ValueOf(actual) + if actualValue.Kind() == reflect.Ptr { + actualValue = actualValue.Elem() + } + if actualValue == (reflect.Value{}) { + return nil, fmt.Errorf("HaveField encountered nil while dereferencing a pointer of type %T.", actual) + } + if actualValue.Kind() != reflect.Struct { return nil, fmt.Errorf("HaveField encountered:\n%s\nWhich is not a struct.", format.Object(actual, 1)) } diff --git a/vendor/golang.org/x/net/http2/client_conn_pool.go b/vendor/golang.org/x/net/http2/client_conn_pool.go index 8fd95bb..c936843 100644 --- a/vendor/golang.org/x/net/http2/client_conn_pool.go +++ b/vendor/golang.org/x/net/http2/client_conn_pool.go @@ -48,7 +48,7 @@ type clientConnPool struct { conns map[string][]*ClientConn // key is host:port dialing map[string]*dialCall // currently in-flight dials keys map[*ClientConn][]string - addConnCalls map[string]*addConnCall // in-flight addConnIfNeede calls + addConnCalls map[string]*addConnCall // in-flight addConnIfNeeded calls } func (p *clientConnPool) GetClientConn(req *http.Request, addr string) (*ClientConn, error) { @@ -60,30 +60,8 @@ const ( noDialOnMiss = false ) -// shouldTraceGetConn reports whether getClientConn should call any -// ClientTrace.GetConn hook associated with the http.Request. -// -// This complexity is needed to avoid double calls of the GetConn hook -// during the back-and-forth between net/http and x/net/http2 (when the -// net/http.Transport is upgraded to also speak http2), as well as support -// the case where x/net/http2 is being used directly. -func (p *clientConnPool) shouldTraceGetConn(cc *ClientConn) bool { - // If our Transport wasn't made via ConfigureTransport, always - // trace the GetConn hook if provided, because that means the - // http2 package is being used directly and it's the one - // dialing, as opposed to net/http. - if _, ok := p.t.ConnPool.(noDialClientConnPool); !ok { - return true - } - // Otherwise, only use the GetConn hook if this connection has - // been used previously for other requests. For fresh - // connections, the net/http package does the dialing. - cc.mu.Lock() - defer cc.mu.Unlock() - return cc.nextStreamID == 1 -} - func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMiss bool) (*ClientConn, error) { + // TODO(dneil): Dial a new connection when t.DisableKeepAlives is set? if isConnectionCloseRequest(req) && dialOnMiss { // It gets its own connection. traceGetConn(req, addr) @@ -98,9 +76,13 @@ func (p *clientConnPool) getClientConn(req *http.Request, addr string, dialOnMis p.mu.Lock() for _, cc := range p.conns[addr] { if cc.ReserveNewRequest() { - if p.shouldTraceGetConn(cc) { + // When a connection is presented to us by the net/http package, + // the GetConn hook has already been called. + // Don't call it a second time here. + if !cc.getConnCalled { traceGetConn(req, addr) } + cc.getConnCalled = false p.mu.Unlock() return cc, nil } @@ -219,6 +201,7 @@ func (c *addConnCall) run(t *Transport, key string, tc *tls.Conn) { if err != nil { c.err = err } else { + cc.getConnCalled = true // already called by the net/http package p.addConnLocked(key, cc) } delete(p.addConnCalls, key) diff --git a/vendor/golang.org/x/net/http2/go118.go b/vendor/golang.org/x/net/http2/go118.go new file mode 100644 index 0000000..aca4b2b --- /dev/null +++ b/vendor/golang.org/x/net/http2/go118.go @@ -0,0 +1,17 @@ +// Copyright 2021 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. + +//go:build go1.18 +// +build go1.18 + +package http2 + +import ( + "crypto/tls" + "net" +) + +func tlsUnderlyingConn(tc *tls.Conn) net.Conn { + return tc.NetConn() +} diff --git a/vendor/golang.org/x/net/http2/hpack/huffman.go b/vendor/golang.org/x/net/http2/hpack/huffman.go index a1ab2f0..fe0b84c 100644 --- a/vendor/golang.org/x/net/http2/hpack/huffman.go +++ b/vendor/golang.org/x/net/http2/hpack/huffman.go @@ -140,25 +140,29 @@ func buildRootHuffmanNode() { panic("unexpected size") } lazyRootHuffmanNode = newInternalNode() - for i, code := range huffmanCodes { - addDecoderNode(byte(i), code, huffmanCodeLen[i]) - } -} + // allocate a leaf node for each of the 256 symbols + leaves := new([256]node) + + for sym, code := range huffmanCodes { + codeLen := huffmanCodeLen[sym] + + cur := lazyRootHuffmanNode + for codeLen > 8 { + codeLen -= 8 + i := uint8(code >> codeLen) + if cur.children[i] == nil { + cur.children[i] = newInternalNode() + } + cur = cur.children[i] + } + shift := 8 - codeLen + start, end := int(uint8(code< 8 { - codeLen -= 8 - i := uint8(code >> codeLen) - if cur.children[i] == nil { - cur.children[i] = newInternalNode() + leaves[sym].sym = byte(sym) + leaves[sym].codeLen = codeLen + for i := start; i < start+end; i++ { + cur.children[i] = &leaves[sym] } - cur = cur.children[i] - } - shift := 8 - codeLen - start, end := int(uint8(code< 0 && errors.Is(err, os.ErrDeadlineExceeded) { + // Keep extending the deadline so long as we're making progress. + continue + } + if sew.timeout != 0 { + sew.conn.SetWriteDeadline(time.Time{}) + } + *sew.err = err + return n, err + } } // noCachedConnError is the concrete type of ErrNoCachedConn, which @@ -505,10 +496,9 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res } reused := !atomic.CompareAndSwapUint32(&cc.reused, 0, 1) traceGotConn(req, cc, reused) - body := req.Body - res, gotErrAfterReqBodyWrite, err := cc.roundTrip(req) + res, err := cc.RoundTrip(req) if err != nil && retry <= 6 { - if req, err = shouldRetryRequest(req, err, gotErrAfterReqBodyWrite); err == nil { + if req, err = shouldRetryRequest(req, err); err == nil { // After the first retry, do exponential backoff with 10% jitter. if retry == 0 { continue @@ -525,11 +515,6 @@ func (t *Transport) RoundTripOpt(req *http.Request, opt RoundTripOpt) (*http.Res } if err != nil { t.vlogf("RoundTrip failure: %v", err) - // If the error occurred after the body write started, - // the body writer will close the body. Otherwise, do so here. - if body != nil && !gotErrAfterReqBodyWrite { - body.Close() - } return nil, err } return res, nil @@ -555,7 +540,7 @@ var ( // response headers. It is always called with a non-nil error. // It returns either a request to retry (either the same request, or a // modified clone), or an error if the request can't be replayed. -func shouldRetryRequest(req *http.Request, err error, afterBodyWrite bool) (*http.Request, error) { +func shouldRetryRequest(req *http.Request, err error) (*http.Request, error) { if !canRetryError(err) { return nil, err } @@ -568,7 +553,6 @@ func shouldRetryRequest(req *http.Request, err error, afterBodyWrite bool) (*htt // If the request body can be reset back to its original // state via the optional req.GetBody, do that. if req.GetBody != nil { - req.Body.Close() body, err := req.GetBody() if err != nil { return nil, err @@ -580,10 +564,8 @@ func shouldRetryRequest(req *http.Request, err error, afterBodyWrite bool) (*htt // The Request.Body can't reset back to the beginning, but we // don't seem to have started to read from it yet, so reuse - // the request directly. The "afterBodyWrite" means the - // bodyWrite process has started, which becomes true before - // the first Read. - if !afterBodyWrite { + // the request directly. + if err == errClientConnUnusable { return req, nil } @@ -696,7 +678,11 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro // TODO: adjust this writer size to account for frame size + // MTU + crypto/tls record padding. - cc.bw = bufio.NewWriter(stickyErrWriter{c, &cc.werr}) + cc.bw = bufio.NewWriter(stickyErrWriter{ + conn: c, + timeout: t.WriteByteTimeout, + err: &cc.werr, + }) cc.br = bufio.NewReader(c) cc.fr = NewFramer(cc.bw, cc.br) if t.CountError != nil { @@ -749,7 +735,6 @@ func (cc *ClientConn) healthCheck() { err := cc.Ping(ctx) if err != nil { cc.closeForLostPing() - cc.t.connPool().MarkDead(cc) return } } @@ -778,10 +763,7 @@ func (cc *ClientConn) setGoAway(f *GoAwayFrame) { last := f.LastStreamID for streamID, cs := range cc.streams { if streamID > last { - select { - case cs.resc <- resAndError{err: errClientConnGotGoAway}: - default: - } + cs.abortStreamLocked(errClientConnGotGoAway) } } } @@ -810,11 +792,65 @@ func (cc *ClientConn) ReserveNewRequest() bool { return true } +// ClientConnState describes the state of a ClientConn. +type ClientConnState struct { + // Closed is whether the connection is closed. + Closed bool + + // Closing is whether the connection is in the process of + // closing. It may be closing due to shutdown, being a + // single-use connection, being marked as DoNotReuse, or + // having received a GOAWAY frame. + Closing bool + + // StreamsActive is how many streams are active. + StreamsActive int + + // StreamsReserved is how many streams have been reserved via + // ClientConn.ReserveNewRequest. + StreamsReserved int + + // StreamsPending is how many requests have been sent in excess + // of the peer's advertised MaxConcurrentStreams setting and + // are waiting for other streams to complete. + StreamsPending int + + // MaxConcurrentStreams is how many concurrent streams the + // peer advertised as acceptable. Zero means no SETTINGS + // frame has been received yet. + MaxConcurrentStreams uint32 + + // LastIdle, if non-zero, is when the connection last + // transitioned to idle state. + LastIdle time.Time +} + +// State returns a snapshot of cc's state. +func (cc *ClientConn) State() ClientConnState { + cc.wmu.Lock() + maxConcurrent := cc.maxConcurrentStreams + if !cc.seenSettings { + maxConcurrent = 0 + } + cc.wmu.Unlock() + + cc.mu.Lock() + defer cc.mu.Unlock() + return ClientConnState{ + Closed: cc.closed, + Closing: cc.closing || cc.singleUse || cc.doNotReuse || cc.goAway != nil, + StreamsActive: len(cc.streams), + StreamsReserved: cc.streamsReserved, + StreamsPending: cc.pendingRequests, + LastIdle: cc.lastIdle, + MaxConcurrentStreams: maxConcurrent, + } +} + // clientConnIdleState describes the suitability of a client // connection to initiate a new RoundTrip request. type clientConnIdleState struct { canTakeNewRequest bool - freshConn bool // whether it's unused by any previous request } func (cc *ClientConn) idleState() clientConnIdleState { @@ -842,7 +878,6 @@ func (cc *ClientConn) idleStateLocked() (st clientConnIdleState) { !cc.doNotReuse && int64(cc.nextStreamID)+2*int64(cc.pendingRequests) < math.MaxInt32 && !cc.tooIdleLocked() - st.freshConn = cc.nextStreamID == 1 && st.canTakeNewRequest return } @@ -871,9 +906,27 @@ func (cc *ClientConn) onIdleTimeout() { cc.closeIfIdle() } +func (cc *ClientConn) closeConn() error { + t := time.AfterFunc(250*time.Millisecond, cc.forceCloseConn) + defer t.Stop() + return cc.tconn.Close() +} + +// A tls.Conn.Close can hang for a long time if the peer is unresponsive. +// Try to shut it down more aggressively. +func (cc *ClientConn) forceCloseConn() { + tc, ok := cc.tconn.(*tls.Conn) + if !ok { + return + } + if nc := tlsUnderlyingConn(tc); nc != nil { + nc.Close() + } +} + func (cc *ClientConn) closeIfIdle() { cc.mu.Lock() - if len(cc.streams) > 0 { + if len(cc.streams) > 0 || cc.streamsReserved > 0 { cc.mu.Unlock() return } @@ -885,7 +938,7 @@ func (cc *ClientConn) closeIfIdle() { if VerboseLogs { cc.vlogf("http2: Transport closing idle conn %p (forSingleUse=%v, maxStream=%v)", cc, cc.singleUse, nextID-2) } - cc.tconn.Close() + cc.closeConn() } func (cc *ClientConn) isDoNotReuseAndIdle() bool { @@ -896,13 +949,13 @@ func (cc *ClientConn) isDoNotReuseAndIdle() bool { var shutdownEnterWaitStateHook = func() {} -// Shutdown gracefully close the client connection, waiting for running streams to complete. +// Shutdown gracefully closes the client connection, waiting for running streams to complete. func (cc *ClientConn) Shutdown(ctx context.Context) error { if err := cc.sendGoAway(); err != nil { return err } // Wait for all in-flight streams to complete or connection to close - done := make(chan error, 1) + done := make(chan struct{}) cancelled := false // guarded by cc.mu go func() { cc.mu.Lock() @@ -910,7 +963,7 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error { for { if len(cc.streams) == 0 || cc.closed { cc.closed = true - done <- cc.tconn.Close() + close(done) break } if cancelled { @@ -921,8 +974,8 @@ func (cc *ClientConn) Shutdown(ctx context.Context) error { }() shutdownEnterWaitStateHook() select { - case err := <-done: - return err + case <-done: + return cc.closeConn() case <-ctx.Done(): cc.mu.Lock() // Free the goroutine above @@ -961,23 +1014,13 @@ func (cc *ClientConn) sendGoAway() error { // err is sent to streams. func (cc *ClientConn) closeForError(err error) error { cc.mu.Lock() - streams := cc.streams - cc.streams = nil cc.closed = true - cc.mu.Unlock() - - for _, cs := range streams { - select { - case cs.resc <- resAndError{err: err}: - default: - } - cs.bufPipe.CloseWithError(err) + for _, cs := range cc.streams { + cs.abortStreamLocked(err) } - - cc.mu.Lock() - defer cc.cond.Broadcast() - defer cc.mu.Unlock() - return cc.tconn.Close() + cc.cond.Broadcast() + cc.mu.Unlock() + return cc.closeConn() } // Close closes the client connection immediately. @@ -1071,70 +1114,145 @@ func (cc *ClientConn) decrStreamReservationsLocked() { } func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) { - resp, _, err := cc.roundTrip(req) - return resp, err -} - -func (cc *ClientConn) roundTrip(req *http.Request) (res *http.Response, gotErrAfterReqBodyWrite bool, err error) { ctx := req.Context() - if err := checkConnHeaders(req); err != nil { - cc.decrStreamReservations() - return nil, false, err + cs := &clientStream{ + cc: cc, + ctx: ctx, + reqCancel: req.Cancel, + isHead: req.Method == "HEAD", + reqBody: req.Body, + reqBodyContentLength: actualContentLength(req), + trace: httptrace.ContextClientTrace(ctx), + peerClosed: make(chan struct{}), + abort: make(chan struct{}), + respHeaderRecv: make(chan struct{}), + donec: make(chan struct{}), + } + go cs.doRequest(req) + + waitDone := func() error { + select { + case <-cs.donec: + return nil + case <-ctx.Done(): + return ctx.Err() + case <-cs.reqCancel: + return errRequestCanceled + } } - if cc.idleTimer != nil { - cc.idleTimer.Stop() + + handleResponseHeaders := func() (*http.Response, error) { + res := cs.res + if res.StatusCode > 299 { + // On error or status code 3xx, 4xx, 5xx, etc abort any + // ongoing write, assuming that the server doesn't care + // about our request body. If the server replied with 1xx or + // 2xx, however, then assume the server DOES potentially + // want our body (e.g. full-duplex streaming: + // golang.org/issue/13444). If it turns out the server + // doesn't, they'll RST_STREAM us soon enough. This is a + // heuristic to avoid adding knobs to Transport. Hopefully + // we can keep it. + cs.abortRequestBodyWrite() + } + res.Request = req + res.TLS = cc.tlsState + if res.Body == noBody && actualContentLength(req) == 0 { + // If there isn't a request or response body still being + // written, then wait for the stream to be closed before + // RoundTrip returns. + if err := waitDone(); err != nil { + return nil, err + } + } + return res, nil } - trailers, err := commaSeparatedTrailers(req) - if err != nil { - cc.decrStreamReservations() - return nil, false, err + for { + select { + case <-cs.respHeaderRecv: + return handleResponseHeaders() + case <-cs.abort: + select { + case <-cs.respHeaderRecv: + // If both cs.respHeaderRecv and cs.abort are signaling, + // pick respHeaderRecv. The server probably wrote the + // response and immediately reset the stream. + // golang.org/issue/49645 + return handleResponseHeaders() + default: + waitDone() + return nil, cs.abortErr + } + case <-ctx.Done(): + err := ctx.Err() + cs.abortStream(err) + return nil, err + case <-cs.reqCancel: + cs.abortStream(errRequestCanceled) + return nil, errRequestCanceled + } + } +} + +// doRequest runs for the duration of the request lifetime. +// +// It sends the request and performs post-request cleanup (closing Request.Body, etc.). +func (cs *clientStream) doRequest(req *http.Request) { + err := cs.writeRequest(req) + cs.cleanupWriteRequest(err) +} + +// writeRequest sends a request. +// +// It returns nil after the request is written, the response read, +// and the request stream is half-closed by the peer. +// +// It returns non-nil if the request ends otherwise. +// If the returned error is StreamError, the error Code may be used in resetting the stream. +func (cs *clientStream) writeRequest(req *http.Request) (err error) { + cc := cs.cc + ctx := cs.ctx + + if err := checkConnHeaders(req); err != nil { + return err } - hasTrailers := trailers != "" // Acquire the new-request lock by writing to reqHeaderMu. // This lock guards the critical section covering allocating a new stream ID // (requires mu) and creating the stream (requires wmu). if cc.reqHeaderMu == nil { - panic("RoundTrip on initialized ClientConn") // for tests + panic("RoundTrip on uninitialized ClientConn") // for tests } select { case cc.reqHeaderMu <- struct{}{}: - case <-req.Cancel: - cc.decrStreamReservations() - return nil, false, errRequestCanceled + case <-cs.reqCancel: + return errRequestCanceled case <-ctx.Done(): - cc.decrStreamReservations() - return nil, false, ctx.Err() + return ctx.Err() } - reqHeaderMuNeedsUnlock := true - defer func() { - if reqHeaderMuNeedsUnlock { - <-cc.reqHeaderMu - } - }() cc.mu.Lock() + if cc.idleTimer != nil { + cc.idleTimer.Stop() + } cc.decrStreamReservationsLocked() - if req.URL == nil { + if err := cc.awaitOpenSlotForStreamLocked(cs); err != nil { cc.mu.Unlock() - return nil, false, errNilRequestURL + <-cc.reqHeaderMu + return err } - if err := cc.awaitOpenSlotForRequest(req); err != nil { - cc.mu.Unlock() - return nil, false, err + cc.addStreamLocked(cs) // assigns stream ID + if isConnectionCloseRequest(req) { + cc.doNotReuse = true } - - body := req.Body - contentLen := actualContentLength(req) - hasBody := contentLen != 0 + cc.mu.Unlock() // TODO(bradfitz): this is a copy of the logic in net/http. Unify somewhere? - var requestedGzip bool if !cc.t.disableCompression() && req.Header.Get("Accept-Encoding") == "" && req.Header.Get("Range") == "" && - req.Method != "HEAD" { + !cs.isHead { // Request gzip only, not deflate. Deflate is ambiguous and // not as universally supported anyway. // See: https://zlib.net/zlib_faq.html#faq39 @@ -1147,185 +1265,224 @@ func (cc *ClientConn) roundTrip(req *http.Request) (res *http.Response, gotErrAf // We don't request gzip if the request is for a range, since // auto-decoding a portion of a gzipped document will just fail // anyway. See https://golang.org/issue/8923 - requestedGzip = true + cs.requestedGzip = true } - cs := cc.newStream() - cs.req = req - cs.trace = httptrace.ContextClientTrace(req.Context()) - cs.requestedGzip = requestedGzip - bodyWriter := cc.t.getBodyWriterState(cs, body) - cs.on100 = bodyWriter.on100 - cc.mu.Unlock() + continueTimeout := cc.t.expectContinueTimeout() + if continueTimeout != 0 { + if !httpguts.HeaderValuesContainsToken(req.Header["Expect"], "100-continue") { + continueTimeout = 0 + } else { + cs.on100 = make(chan struct{}, 1) + } + } + // Past this point (where we send request headers), it is possible for + // RoundTrip to return successfully. Since the RoundTrip contract permits + // the caller to "mutate or reuse" the Request after closing the Response's Body, + // we must take care when referencing the Request from here on. + err = cs.encodeAndWriteHeaders(req) + <-cc.reqHeaderMu + if err != nil { + return err + } + + hasBody := cs.reqBodyContentLength != 0 + if !hasBody { + cs.sentEndStream = true + } else { + if continueTimeout != 0 { + traceWait100Continue(cs.trace) + timer := time.NewTimer(continueTimeout) + select { + case <-timer.C: + err = nil + case <-cs.on100: + err = nil + case <-cs.abort: + err = cs.abortErr + case <-ctx.Done(): + err = ctx.Err() + case <-cs.reqCancel: + err = errRequestCanceled + } + timer.Stop() + if err != nil { + traceWroteRequest(cs.trace, err) + return err + } + } + + if err = cs.writeRequestBody(req); err != nil { + if err != errStopReqBodyWrite { + traceWroteRequest(cs.trace, err) + return err + } + } else { + cs.sentEndStream = true + } + } + + traceWroteRequest(cs.trace, err) + + var respHeaderTimer <-chan time.Time + var respHeaderRecv chan struct{} + if d := cc.responseHeaderTimeout(); d != 0 { + timer := time.NewTimer(d) + defer timer.Stop() + respHeaderTimer = timer.C + respHeaderRecv = cs.respHeaderRecv + } + // Wait until the peer half-closes its end of the stream, + // or until the request is aborted (via context, error, or otherwise), + // whichever comes first. + for { + select { + case <-cs.peerClosed: + return nil + case <-respHeaderTimer: + return errTimeout + case <-respHeaderRecv: + respHeaderRecv = nil + respHeaderTimer = nil // keep waiting for END_STREAM + case <-cs.abort: + return cs.abortErr + case <-ctx.Done(): + return ctx.Err() + case <-cs.reqCancel: + return errRequestCanceled + } + } +} + +func (cs *clientStream) encodeAndWriteHeaders(req *http.Request) error { + cc := cs.cc + ctx := cs.ctx + + cc.wmu.Lock() + defer cc.wmu.Unlock() + + // If the request was canceled while waiting for cc.mu, just quit. + select { + case <-cs.abort: + return cs.abortErr + case <-ctx.Done(): + return ctx.Err() + case <-cs.reqCancel: + return errRequestCanceled + default: + } + + // Encode headers. + // // we send: HEADERS{1}, CONTINUATION{0,} + DATA{0,} (DATA is // sent by writeRequestBody below, along with any Trailers, // again in form HEADERS{1}, CONTINUATION{0,}) - cc.wmu.Lock() - hdrs, err := cc.encodeHeaders(req, requestedGzip, trailers, contentLen) + trailers, err := commaSeparatedTrailers(req) if err != nil { - cc.wmu.Unlock() - return nil, false, err + return err + } + hasTrailers := trailers != "" + contentLen := actualContentLength(req) + hasBody := contentLen != 0 + hdrs, err := cc.encodeHeaders(req, cs.requestedGzip, trailers, contentLen) + if err != nil { + return err } - defer func() { - cc.wmu.Lock() - werr := cc.werr - cc.wmu.Unlock() - if werr != nil { - cc.Close() - } - }() - + // Write the request. endStream := !hasBody && !hasTrailers + cs.sentHeaders = true err = cc.writeHeaders(cs.ID, endStream, int(cc.maxFrameSize), hdrs) - cc.wmu.Unlock() - <-cc.reqHeaderMu // release the new-request lock - reqHeaderMuNeedsUnlock = false traceWroteHeaders(cs.trace) + return err +} - if err != nil { - if hasBody { - bodyWriter.cancel() - } - cc.forgetStreamID(cs.ID) - // Don't bother sending a RST_STREAM (our write already failed; - // no need to keep writing) - traceWroteRequest(cs.trace, err) - // TODO(dneil): An error occurred while writing the headers. - // Should we return an error indicating that this request can be retried? - return nil, false, err - } +// cleanupWriteRequest performs post-request tasks. +// +// If err (the result of writeRequest) is non-nil and the stream is not closed, +// cleanupWriteRequest will send a reset to the peer. +func (cs *clientStream) cleanupWriteRequest(err error) { + cc := cs.cc - var respHeaderTimer <-chan time.Time - if hasBody { - bodyWriter.scheduleBodyWrite() - } else { - traceWroteRequest(cs.trace, nil) - if d := cc.responseHeaderTimeout(); d != 0 { - timer := time.NewTimer(d) - defer timer.Stop() - respHeaderTimer = timer.C - } + if cs.ID == 0 { + // We were canceled before creating the stream, so return our reservation. + cc.decrStreamReservations() } - readLoopResCh := cs.resc - bodyWritten := false + // TODO: write h12Compare test showing whether + // Request.Body is closed by the Transport, + // and in multiple cases: server replies <=299 and >299 + // while still writing request body + cc.mu.Lock() + bodyClosed := cs.reqBodyClosed + cs.reqBodyClosed = true + cc.mu.Unlock() + if !bodyClosed && cs.reqBody != nil { + cs.reqBody.Close() + } - handleReadLoopResponse := func(re resAndError) (*http.Response, bool, error) { - res := re.res - if re.err != nil || res.StatusCode > 299 { - // On error or status code 3xx, 4xx, 5xx, etc abort any - // ongoing write, assuming that the server doesn't care - // about our request body. If the server replied with 1xx or - // 2xx, however, then assume the server DOES potentially - // want our body (e.g. full-duplex streaming: - // golang.org/issue/13444). If it turns out the server - // doesn't, they'll RST_STREAM us soon enough. This is a - // heuristic to avoid adding knobs to Transport. Hopefully - // we can keep it. - bodyWriter.cancel() - cs.abortRequestBodyWrite(errStopReqBodyWrite) - if hasBody && !bodyWritten { - <-bodyWriter.resc + if err != nil && cs.sentEndStream { + // If the connection is closed immediately after the response is read, + // we may be aborted before finishing up here. If the stream was closed + // cleanly on both sides, there is no error. + select { + case <-cs.peerClosed: + err = nil + default: + } + } + if err != nil { + cs.abortStream(err) // possibly redundant, but harmless + if cs.sentHeaders { + if se, ok := err.(StreamError); ok { + if se.Cause != errFromPeer { + cc.writeStreamReset(cs.ID, se.Code, err) + } + } else { + cc.writeStreamReset(cs.ID, ErrCodeCancel, err) } } - if re.err != nil { - cc.forgetStreamID(cs.ID) - return nil, cs.getStartedWrite(), re.err + cs.bufPipe.CloseWithError(err) // no-op if already closed + } else { + if cs.sentHeaders && !cs.sentEndStream { + cc.writeStreamReset(cs.ID, ErrCodeNo, nil) } - res.Request = req - res.TLS = cc.tlsState - return res, false, nil + cs.bufPipe.CloseWithError(errRequestCanceled) } - - handleError := func(err error) (*http.Response, bool, error) { - if !hasBody || bodyWritten { - cc.writeStreamReset(cs.ID, ErrCodeCancel, nil) - } else { - bodyWriter.cancel() - cs.abortRequestBodyWrite(errStopReqBodyWriteAndCancel) - <-bodyWriter.resc - } + if cs.ID != 0 { cc.forgetStreamID(cs.ID) - return nil, cs.getStartedWrite(), err } - for { - select { - case re := <-readLoopResCh: - return handleReadLoopResponse(re) - case <-respHeaderTimer: - return handleError(errTimeout) - case <-ctx.Done(): - return handleError(ctx.Err()) - case <-req.Cancel: - return handleError(errRequestCanceled) - case <-cs.peerReset: - // processResetStream already removed the - // stream from the streams map; no need for - // forgetStreamID. - return nil, cs.getStartedWrite(), cs.resetErr - case err := <-bodyWriter.resc: - bodyWritten = true - // Prefer the read loop's response, if available. Issue 16102. - select { - case re := <-readLoopResCh: - return handleReadLoopResponse(re) - default: - } - if err != nil { - cc.forgetStreamID(cs.ID) - return nil, cs.getStartedWrite(), err - } - if d := cc.responseHeaderTimeout(); d != 0 { - timer := time.NewTimer(d) - defer timer.Stop() - respHeaderTimer = timer.C - } - } + cc.wmu.Lock() + werr := cc.werr + cc.wmu.Unlock() + if werr != nil { + cc.Close() } + + close(cs.donec) } -// awaitOpenSlotForRequest waits until len(streams) < maxConcurrentStreams. +// awaitOpenSlotForStream waits until len(streams) < maxConcurrentStreams. // Must hold cc.mu. -func (cc *ClientConn) awaitOpenSlotForRequest(req *http.Request) error { - var waitingForConn chan struct{} - var waitingForConnErr error // guarded by cc.mu +func (cc *ClientConn) awaitOpenSlotForStreamLocked(cs *clientStream) error { for { cc.lastActive = time.Now() if cc.closed || !cc.canTakeNewRequestLocked() { - if waitingForConn != nil { - close(waitingForConn) - } return errClientConnUnusable } cc.lastIdle = time.Time{} if int64(len(cc.streams)) < int64(cc.maxConcurrentStreams) { - if waitingForConn != nil { - close(waitingForConn) - } return nil } - // Unfortunately, we cannot wait on a condition variable and channel at - // the same time, so instead, we spin up a goroutine to check if the - // request is canceled while we wait for a slot to open in the connection. - if waitingForConn == nil { - waitingForConn = make(chan struct{}) - go func() { - if err := awaitRequestCancel(req, waitingForConn); err != nil { - cc.mu.Lock() - waitingForConnErr = err - cc.cond.Broadcast() - cc.mu.Unlock() - } - }() - } cc.pendingRequests++ cc.cond.Wait() cc.pendingRequests-- - if waitingForConnErr != nil { - return waitingForConnErr + select { + case <-cs.abort: + return cs.abortErr + default: } } } @@ -1352,10 +1509,6 @@ func (cc *ClientConn) writeHeaders(streamID uint32, endStream bool, maxFrameSize cc.fr.WriteContinuation(streamID, endHeaders, chunk) } } - // TODO(bradfitz): this Flush could potentially block (as - // could the WriteHeaders call(s) above), which means they - // wouldn't respond to Request.Cancel being readable. That's - // rare, but this should probably be in a goroutine. cc.bw.Flush() return cc.werr } @@ -1382,7 +1535,7 @@ func (cs *clientStream) frameScratchBufferLen(maxFrameSize int) int { if n > max { n = max } - if cl := actualContentLength(cs.req); cl != -1 && cl+1 < n { + if cl := cs.reqBodyContentLength; cl != -1 && cl+1 < n { // Add an extra byte past the declared content-length to // give the caller's Request.Body io.Reader a chance to // give us more bytes than they declared, so we can catch it @@ -1397,31 +1550,13 @@ func (cs *clientStream) frameScratchBufferLen(maxFrameSize int) int { var bufPool sync.Pool // of *[]byte -func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) (err error) { +func (cs *clientStream) writeRequestBody(req *http.Request) (err error) { cc := cs.cc + body := cs.reqBody sentEnd := false // whether we sent the final DATA frame w/ END_STREAM - defer func() { - traceWroteRequest(cs.trace, err) - // TODO: write h12Compare test showing whether - // Request.Body is closed by the Transport, - // and in multiple cases: server replies <=299 and >299 - // while still writing request body - var cerr error - cc.mu.Lock() - if cs.stopReqBody == nil { - cs.stopReqBody = errStopReqBodyWrite - cerr = bodyCloser.Close() - } - cc.mu.Unlock() - if err == nil { - err = cerr - } - }() - - req := cs.req hasTrailers := req.Trailer != nil - remainLen := actualContentLength(req) + remainLen := cs.reqBodyContentLength hasContentLen := remainLen != -1 cc.mu.Lock() @@ -1459,29 +1594,29 @@ func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) ( } if remainLen < 0 { err = errReqBodyTooLong - cc.writeStreamReset(cs.ID, ErrCodeCancel, err) return err } } - if err == io.EOF { - sawEOF = true - err = nil - } else if err != nil { - cc.writeStreamReset(cs.ID, ErrCodeCancel, err) - return err + if err != nil { + cc.mu.Lock() + bodyClosed := cs.reqBodyClosed + cc.mu.Unlock() + switch { + case bodyClosed: + return errStopReqBodyWrite + case err == io.EOF: + sawEOF = true + err = nil + default: + return err + } } remain := buf[:n] for len(remain) > 0 && err == nil { var allowed int32 allowed, err = cs.awaitFlowControl(len(remain)) - switch { - case err == errStopReqBodyWrite: - return err - case err == errStopReqBodyWriteAndCancel: - cc.writeStreamReset(cs.ID, ErrCodeCancel, nil) - return err - case err != nil: + if err != nil { return err } cc.wmu.Lock() @@ -1512,18 +1647,26 @@ func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) ( return nil } + // Since the RoundTrip contract permits the caller to "mutate or reuse" + // a request after the Response's Body is closed, verify that this hasn't + // happened before accessing the trailers. + cc.mu.Lock() + trailer := req.Trailer + err = cs.abortErr + cc.mu.Unlock() + if err != nil { + return err + } + cc.wmu.Lock() + defer cc.wmu.Unlock() var trls []byte - if hasTrailers { - trls, err = cc.encodeTrailers(req) + if len(trailer) > 0 { + trls, err = cc.encodeTrailers(trailer) if err != nil { - cc.wmu.Unlock() - cc.writeStreamReset(cs.ID, ErrCodeInternal, err) - cc.forgetStreamID(cs.ID) return err } } - defer cc.wmu.Unlock() // Two ways to send END_STREAM: either with trailers, or // with an empty DATA frame. @@ -1544,17 +1687,24 @@ func (cs *clientStream) writeRequestBody(body io.Reader, bodyCloser io.Closer) ( // if the stream is dead. func (cs *clientStream) awaitFlowControl(maxBytes int) (taken int32, err error) { cc := cs.cc + ctx := cs.ctx cc.mu.Lock() defer cc.mu.Unlock() for { if cc.closed { return 0, errClientConnClosed } - if cs.stopReqBody != nil { - return 0, cs.stopReqBody + if cs.reqBodyClosed { + return 0, errStopReqBodyWrite } - if err := cs.checkResetOrDone(); err != nil { - return 0, err + select { + case <-cs.abort: + return 0, cs.abortErr + case <-ctx.Done(): + return 0, ctx.Err() + case <-cs.reqCancel: + return 0, errRequestCanceled + default: } if a := cs.flow.available(); a > 0 { take := a @@ -1766,11 +1916,11 @@ func shouldSendReqContentLength(method string, contentLength int64) bool { } // requires cc.wmu be held. -func (cc *ClientConn) encodeTrailers(req *http.Request) ([]byte, error) { +func (cc *ClientConn) encodeTrailers(trailer http.Header) ([]byte, error) { cc.hbuf.Reset() hlSize := uint64(0) - for k, vv := range req.Trailer { + for k, vv := range trailer { for _, v := range vv { hf := hpack.HeaderField{Name: k, Value: v} hlSize += uint64(hf.Size()) @@ -1780,7 +1930,7 @@ func (cc *ClientConn) encodeTrailers(req *http.Request) ([]byte, error) { return nil, errRequestHeaderListSize } - for k, vv := range req.Trailer { + for k, vv := range trailer { lowKey, ascii := asciiToLower(k) if !ascii { // Skip writing invalid headers. Per RFC 7540, Section 8.1.2, header @@ -1810,51 +1960,51 @@ type resAndError struct { } // requires cc.mu be held. -func (cc *ClientConn) newStream() *clientStream { - cs := &clientStream{ - cc: cc, - ID: cc.nextStreamID, - resc: make(chan resAndError, 1), - peerReset: make(chan struct{}), - done: make(chan struct{}), - } +func (cc *ClientConn) addStreamLocked(cs *clientStream) { cs.flow.add(int32(cc.initialWindowSize)) cs.flow.setConnFlow(&cc.flow) cs.inflow.add(transportDefaultStreamFlow) cs.inflow.setConnFlow(&cc.inflow) + cs.ID = cc.nextStreamID cc.nextStreamID += 2 cc.streams[cs.ID] = cs - return cs + if cs.ID == 0 { + panic("assigned stream ID 0") + } } func (cc *ClientConn) forgetStreamID(id uint32) { - cc.streamByID(id, true) -} - -func (cc *ClientConn) streamByID(id uint32, andRemove bool) *clientStream { cc.mu.Lock() - defer cc.mu.Unlock() - cs := cc.streams[id] - if andRemove && cs != nil && !cc.closed { - cc.lastActive = time.Now() - delete(cc.streams, id) - if len(cc.streams) == 0 && cc.idleTimer != nil { - cc.idleTimer.Reset(cc.idleTimeout) - cc.lastIdle = time.Now() - } - close(cs.done) - // Wake up checkResetOrDone via clientStream.awaitFlowControl and - // wake up RoundTrip if there is a pending request. - cc.cond.Broadcast() + slen := len(cc.streams) + delete(cc.streams, id) + if len(cc.streams) != slen-1 { + panic("forgetting unknown stream id") + } + cc.lastActive = time.Now() + if len(cc.streams) == 0 && cc.idleTimer != nil { + cc.idleTimer.Reset(cc.idleTimeout) + cc.lastIdle = time.Now() + } + // Wake up writeRequestBody via clientStream.awaitFlowControl and + // wake up RoundTrip if there is a pending request. + cc.cond.Broadcast() + + closeOnIdle := cc.singleUse || cc.doNotReuse || cc.t.disableKeepAlives() + if closeOnIdle && cc.streamsReserved == 0 && len(cc.streams) == 0 { + if VerboseLogs { + cc.vlogf("http2: Transport closing idle conn %p (forSingleUse=%v, maxStream=%v)", cc, cc.singleUse, cc.nextStreamID-2) + } + cc.closed = true + defer cc.closeConn() } - return cs + + cc.mu.Unlock() } // clientConnReadLoop is the state owned by the clientConn's frame-reading readLoop. type clientConnReadLoop struct { - _ incomparable - cc *ClientConn - closeWhenIdle bool + _ incomparable + cc *ClientConn } // readLoop runs in its own goroutine and reads and dispatches frames. @@ -1892,8 +2042,8 @@ func isEOFOrNetReadError(err error) bool { func (rl *clientConnReadLoop) cleanup() { cc := rl.cc - defer cc.tconn.Close() - defer cc.t.connPool().MarkDead(cc) + cc.t.connPool().MarkDead(cc) + defer cc.closeConn() defer close(cc.readerDone) if cc.idleTimer != nil { @@ -1915,18 +2065,15 @@ func (rl *clientConnReadLoop) cleanup() { err = io.ErrUnexpectedEOF } cc.closed = true - streams := cc.streams - cc.streams = nil - cc.mu.Unlock() - for _, cs := range streams { - cs.bufPipe.CloseWithError(err) // no-op if already closed + for _, cs := range cc.streams { select { - case cs.resc <- resAndError{err: err}: + case <-cs.peerClosed: + // The server closed the stream before closing the conn, + // so no need to interrupt it. default: + cs.abortStreamLocked(err) } - close(cs.done) } - cc.mu.Lock() cc.cond.Broadcast() cc.mu.Unlock() } @@ -1960,8 +2107,6 @@ func (cc *ClientConn) countReadFrameError(err error) { func (rl *clientConnReadLoop) run() error { cc := rl.cc - rl.closeWhenIdle = cc.t.disableKeepAlives() || cc.singleUse - gotReply := false // ever saw a HEADERS reply gotSettings := false readIdleTimeout := cc.t.ReadIdleTimeout var t *time.Timer @@ -1978,9 +2123,7 @@ func (rl *clientConnReadLoop) run() error { cc.vlogf("http2: Transport readFrame error on conn %p: (%T) %v", cc, err, err) } if se, ok := err.(StreamError); ok { - if cs := cc.streamByID(se.StreamID, false); cs != nil { - cs.cc.writeStreamReset(cs.ID, se.Code, err) - cs.cc.forgetStreamID(cs.ID) + if cs := rl.streamByID(se.StreamID); cs != nil { if se.Cause == nil { se.Cause = cc.fr.errDetail } @@ -2001,22 +2144,16 @@ func (rl *clientConnReadLoop) run() error { } gotSettings = true } - maybeIdle := false // whether frame might transition us to idle switch f := f.(type) { case *MetaHeadersFrame: err = rl.processHeaders(f) - maybeIdle = true - gotReply = true case *DataFrame: err = rl.processData(f) - maybeIdle = true case *GoAwayFrame: err = rl.processGoAway(f) - maybeIdle = true case *RSTStreamFrame: err = rl.processResetStream(f) - maybeIdle = true case *SettingsFrame: err = rl.processSettings(f) case *PushPromiseFrame: @@ -2034,38 +2171,24 @@ func (rl *clientConnReadLoop) run() error { } return err } - if rl.closeWhenIdle && gotReply && maybeIdle { - cc.closeIfIdle() - } } } func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error { - cc := rl.cc - cs := cc.streamByID(f.StreamID, false) + cs := rl.streamByID(f.StreamID) if cs == nil { // We'd get here if we canceled a request while the // server had its response still in flight. So if this // was just something we canceled, ignore it. return nil } - if f.StreamEnded() { - // Issue 20521: If the stream has ended, streamByID() causes - // clientStream.done to be closed, which causes the request's bodyWriter - // to be closed with an errStreamClosed, which may be received by - // clientConn.RoundTrip before the result of processing these headers. - // Deferring stream closure allows the header processing to occur first. - // clientConn.RoundTrip may still receive the bodyWriter error first, but - // the fix for issue 16102 prioritises any response. - // - // Issue 22413: If there is no request body, we should close the - // stream before writing to cs.resc so that the stream is closed - // immediately once RoundTrip returns. - if cs.req.Body != nil { - defer cc.forgetStreamID(f.StreamID) - } else { - cc.forgetStreamID(f.StreamID) - } + if cs.readClosed { + rl.endStreamError(cs, StreamError{ + StreamID: f.StreamID, + Code: ErrCodeProtocol, + Cause: errors.New("protocol error: headers after END_STREAM"), + }) + return nil } if !cs.firstByte { if cs.trace != nil { @@ -2089,9 +2212,11 @@ func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error { return err } // Any other error type is a stream error. - cs.cc.writeStreamReset(f.StreamID, ErrCodeProtocol, err) - cc.forgetStreamID(cs.ID) - cs.resc <- resAndError{err: err} + rl.endStreamError(cs, StreamError{ + StreamID: f.StreamID, + Code: ErrCodeProtocol, + Cause: err, + }) return nil // return nil from process* funcs to keep conn alive } if res == nil { @@ -2099,7 +2224,11 @@ func (rl *clientConnReadLoop) processHeaders(f *MetaHeadersFrame) error { return nil } cs.resTrailer = &res.Trailer - cs.resc <- resAndError{res: res} + cs.res = res + close(cs.respHeaderRecv) + if f.StreamEnded() { + rl.endStream(cs) + } return nil } @@ -2161,6 +2290,9 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra } if statusCode >= 100 && statusCode <= 199 { + if f.StreamEnded() { + return nil, errors.New("1xx informational response with END_STREAM flag") + } cs.num1xx++ const max1xxResponses = 5 // arbitrary bound on number of informational responses, same as net/http if cs.num1xx > max1xxResponses { @@ -2173,42 +2305,49 @@ func (rl *clientConnReadLoop) handleResponse(cs *clientStream, f *MetaHeadersFra } if statusCode == 100 { traceGot100Continue(cs.trace) - if cs.on100 != nil { - cs.on100() // forces any write delay timer to fire + select { + case cs.on100 <- struct{}{}: + default: } } cs.pastHeaders = false // do it all again return nil, nil } - streamEnded := f.StreamEnded() - isHead := cs.req.Method == "HEAD" - if !streamEnded || isHead { - res.ContentLength = -1 - if clens := res.Header["Content-Length"]; len(clens) == 1 { - if cl, err := strconv.ParseUint(clens[0], 10, 63); err == nil { - res.ContentLength = int64(cl) - } else { - // TODO: care? unlike http/1, it won't mess up our framing, so it's - // more safe smuggling-wise to ignore. - } - } else if len(clens) > 1 { + res.ContentLength = -1 + if clens := res.Header["Content-Length"]; len(clens) == 1 { + if cl, err := strconv.ParseUint(clens[0], 10, 63); err == nil { + res.ContentLength = int64(cl) + } else { // TODO: care? unlike http/1, it won't mess up our framing, so it's // more safe smuggling-wise to ignore. } + } else if len(clens) > 1 { + // TODO: care? unlike http/1, it won't mess up our framing, so it's + // more safe smuggling-wise to ignore. + } else if f.StreamEnded() && !cs.isHead { + res.ContentLength = 0 } - if streamEnded || isHead { + if cs.isHead { res.Body = noBody return res, nil } - cs.bufPipe = pipe{b: &dataBuffer{expected: res.ContentLength}} + if f.StreamEnded() { + if res.ContentLength > 0 { + res.Body = missingBody{} + } else { + res.Body = noBody + } + return res, nil + } + + cs.bufPipe.setBuffer(&dataBuffer{expected: res.ContentLength}) cs.bytesRemain = res.ContentLength res.Body = transportResponseBody{cs} - go cs.awaitRequestCancel(cs.req) - if cs.requestedGzip && res.Header.Get("Content-Encoding") == "gzip" { + if cs.requestedGzip && asciiEqualFold(res.Header.Get("Content-Encoding"), "gzip") { res.Header.Del("Content-Encoding") res.Header.Del("Content-Length") res.ContentLength = -1 @@ -2247,8 +2386,7 @@ func (rl *clientConnReadLoop) processTrailers(cs *clientStream, f *MetaHeadersFr } // transportResponseBody is the concrete type of Transport.RoundTrip's -// Response.Body. It is an io.ReadCloser. On Read, it reads from cs.body. -// On Close it sends RST_STREAM if EOF wasn't already seen. +// Response.Body. It is an io.ReadCloser. type transportResponseBody struct { cs *clientStream } @@ -2266,7 +2404,7 @@ func (b transportResponseBody) Read(p []byte) (n int, err error) { n = int(cs.bytesRemain) if err == nil { err = errors.New("net/http: server replied with more than declared Content-Length; truncated") - cc.writeStreamReset(cs.ID, ErrCodeProtocol, err) + cs.abortStream(err) } cs.readErr = err return int(cs.bytesRemain), err @@ -2322,24 +2460,18 @@ func (b transportResponseBody) Close() error { cs := b.cs cc := cs.cc - serverSentStreamEnd := cs.bufPipe.Err() == io.EOF unread := cs.bufPipe.Len() - - if unread > 0 || !serverSentStreamEnd { + if unread > 0 { cc.mu.Lock() - if !serverSentStreamEnd { - cs.didReset = true - } // Return connection-level flow control. if unread > 0 { cc.inflow.add(int32(unread)) } cc.mu.Unlock() + // TODO(dneil): Acquiring this mutex can block indefinitely. + // Move flow control return to a goroutine? cc.wmu.Lock() - if !serverSentStreamEnd { - cc.fr.WriteRSTStream(cs.ID, ErrCodeCancel) - } // Return connection-level flow control. if unread > 0 { cc.fr.WriteWindowUpdate(0, uint32(unread)) @@ -2349,16 +2481,24 @@ func (b transportResponseBody) Close() error { } cs.bufPipe.BreakWithError(errClosedResponseBody) - cc.forgetStreamID(cs.ID) + cs.abortStream(errClosedResponseBody) + + select { + case <-cs.donec: + case <-cs.ctx.Done(): + // See golang/go#49366: The net/http package can cancel the + // request context after the response body is fully read. + // Don't treat this as an error. + return nil + case <-cs.reqCancel: + return errRequestCanceled + } return nil } func (rl *clientConnReadLoop) processData(f *DataFrame) error { cc := rl.cc - cs := cc.streamByID(f.StreamID, f.StreamEnded()) - if f.StreamEnded() && cc.isDoNotReuseAndIdle() { - rl.closeWhenIdle = true - } + cs := rl.streamByID(f.StreamID) data := f.Data() if cs == nil { cc.mu.Lock() @@ -2387,6 +2527,14 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error { } return nil } + if cs.readClosed { + cc.logf("protocol error: received DATA after END_STREAM") + rl.endStreamError(cs, StreamError{ + StreamID: f.StreamID, + Code: ErrCodeProtocol, + }) + return nil + } if !cs.firstByte { cc.logf("protocol error: received DATA before a HEADERS frame") rl.endStreamError(cs, StreamError{ @@ -2396,7 +2544,7 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error { return nil } if f.Length > 0 { - if cs.req.Method == "HEAD" && len(data) > 0 { + if cs.isHead && len(data) > 0 { cc.logf("protocol error: received DATA on a HEAD request") rl.endStreamError(cs, StreamError{ StreamID: f.StreamID, @@ -2418,12 +2566,18 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error { if pad := int(f.Length) - len(data); pad > 0 { refund += pad } - // Return len(data) now if the stream is already closed, - // since data will never be read. - didReset := cs.didReset - if didReset { - refund += len(data) + + didReset := false + var err error + if len(data) > 0 { + if _, err = cs.bufPipe.Write(data); err != nil { + // Return len(data) now if the stream is already closed, + // since data will never be read. + didReset = true + refund += len(data) + } } + if refund > 0 { cc.inflow.add(int32(refund)) if !didReset { @@ -2442,11 +2596,9 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error { cc.wmu.Unlock() } - if len(data) > 0 && !didReset { - if _, err := cs.bufPipe.Write(data); err != nil { - rl.endStreamError(cs, err) - return err - } + if err != nil { + rl.endStreamError(cs, err) + return nil } } @@ -2459,24 +2611,32 @@ func (rl *clientConnReadLoop) processData(f *DataFrame) error { func (rl *clientConnReadLoop) endStream(cs *clientStream) { // TODO: check that any declared content-length matches, like // server.go's (*stream).endStream method. - rl.endStreamError(cs, nil) + if !cs.readClosed { + cs.readClosed = true + // Close cs.bufPipe and cs.peerClosed with cc.mu held to avoid a + // race condition: The caller can read io.EOF from Response.Body + // and close the body before we close cs.peerClosed, causing + // cleanupWriteRequest to send a RST_STREAM. + rl.cc.mu.Lock() + defer rl.cc.mu.Unlock() + cs.bufPipe.closeWithErrorAndCode(io.EOF, cs.copyTrailers) + close(cs.peerClosed) + } } func (rl *clientConnReadLoop) endStreamError(cs *clientStream, err error) { - var code func() - if err == nil { - err = io.EOF - code = cs.copyTrailers - } - if isConnectionCloseRequest(cs.req) { - rl.closeWhenIdle = true - } - cs.bufPipe.closeWithErrorAndCode(err, code) + cs.readAborted = true + cs.abortStream(err) +} - select { - case cs.resc <- resAndError{err: err}: - default: +func (rl *clientConnReadLoop) streamByID(id uint32) *clientStream { + rl.cc.mu.Lock() + defer rl.cc.mu.Unlock() + cs := rl.cc.streams[id] + if cs != nil && !cs.readAborted { + return cs } + return nil } func (cs *clientStream) copyTrailers() { @@ -2589,7 +2749,7 @@ func (rl *clientConnReadLoop) processSettingsNoWrite(f *SettingsFrame) error { func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { cc := rl.cc - cs := cc.streamByID(f.StreamID, false) + cs := rl.streamByID(f.StreamID) if f.StreamID != 0 && cs == nil { return nil } @@ -2609,36 +2769,22 @@ func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { } func (rl *clientConnReadLoop) processResetStream(f *RSTStreamFrame) error { - cc := rl.cc - cs := cc.streamByID(f.StreamID, true) + cs := rl.streamByID(f.StreamID) if cs == nil { - // TODO: return error if server tries to RST_STEAM an idle stream + // TODO: return error if server tries to RST_STREAM an idle stream return nil } - if cc.isDoNotReuseAndIdle() { - rl.closeWhenIdle = true + serr := streamError(cs.ID, f.ErrCode) + serr.Cause = errFromPeer + if f.ErrCode == ErrCodeProtocol { + rl.cc.SetDoNotReuse() } - select { - case <-cs.peerReset: - // Already reset. - // This is the only goroutine - // which closes this, so there - // isn't a race. - default: - serr := streamError(cs.ID, f.ErrCode) - if f.ErrCode == ErrCodeProtocol { - rl.cc.SetDoNotReuse() - serr.Cause = errFromPeer - rl.closeWhenIdle = true - } - if fn := cs.cc.t.CountError; fn != nil { - fn("recv_rststream_" + f.ErrCode.stringToken()) - } - cs.resetErr = serr - close(cs.peerReset) - cs.bufPipe.CloseWithError(serr) - cs.cc.cond.Broadcast() // wake up checkResetOrDone via clientStream.awaitFlowControl + if fn := cs.cc.t.CountError; fn != nil { + fn("recv_rststream_" + f.ErrCode.stringToken()) } + cs.abortStream(serr) + + cs.bufPipe.CloseWithError(serr) return nil } @@ -2660,19 +2806,24 @@ func (cc *ClientConn) Ping(ctx context.Context) error { } cc.mu.Unlock() } - cc.wmu.Lock() - if err := cc.fr.WritePing(false, p); err != nil { - cc.wmu.Unlock() - return err - } - if err := cc.bw.Flush(); err != nil { - cc.wmu.Unlock() - return err - } - cc.wmu.Unlock() + errc := make(chan error, 1) + go func() { + cc.wmu.Lock() + defer cc.wmu.Unlock() + if err := cc.fr.WritePing(false, p); err != nil { + errc <- err + return + } + if err := cc.bw.Flush(); err != nil { + errc <- err + return + } + }() select { case <-c: return nil + case err := <-errc: + return err case <-ctx.Done(): return ctx.Err() case <-cc.readerDone: @@ -2749,6 +2900,11 @@ func (t *Transport) logf(format string, args ...interface{}) { var noBody io.ReadCloser = ioutil.NopCloser(bytes.NewReader(nil)) +type missingBody struct{} + +func (missingBody) Close() error { return nil } +func (missingBody) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF } + func strSliceContains(ss []string, s string) bool { for _, v := range ss { if v == s { @@ -2794,87 +2950,6 @@ type errorReader struct{ err error } func (r errorReader) Read(p []byte) (int, error) { return 0, r.err } -// bodyWriterState encapsulates various state around the Transport's writing -// of the request body, particularly regarding doing delayed writes of the body -// when the request contains "Expect: 100-continue". -type bodyWriterState struct { - cs *clientStream - timer *time.Timer // if non-nil, we're doing a delayed write - fnonce *sync.Once // to call fn with - fn func() // the code to run in the goroutine, writing the body - resc chan error // result of fn's execution - delay time.Duration // how long we should delay a delayed write for -} - -func (t *Transport) getBodyWriterState(cs *clientStream, body io.Reader) (s bodyWriterState) { - s.cs = cs - if body == nil { - return - } - resc := make(chan error, 1) - s.resc = resc - s.fn = func() { - cs.cc.mu.Lock() - cs.startedWrite = true - cs.cc.mu.Unlock() - resc <- cs.writeRequestBody(body, cs.req.Body) - } - s.delay = t.expectContinueTimeout() - if s.delay == 0 || - !httpguts.HeaderValuesContainsToken( - cs.req.Header["Expect"], - "100-continue") { - return - } - s.fnonce = new(sync.Once) - - // Arm the timer with a very large duration, which we'll - // intentionally lower later. It has to be large now because - // we need a handle to it before writing the headers, but the - // s.delay value is defined to not start until after the - // request headers were written. - const hugeDuration = 365 * 24 * time.Hour - s.timer = time.AfterFunc(hugeDuration, func() { - s.fnonce.Do(s.fn) - }) - return -} - -func (s bodyWriterState) cancel() { - if s.timer != nil { - if s.timer.Stop() { - s.resc <- nil - } - } -} - -func (s bodyWriterState) on100() { - if s.timer == nil { - // If we didn't do a delayed write, ignore the server's - // bogus 100 continue response. - return - } - s.timer.Stop() - go func() { s.fnonce.Do(s.fn) }() -} - -// scheduleBodyWrite starts writing the body, either immediately (in -// the common case) or after the delay timeout. It should not be -// called until after the headers have been written. -func (s bodyWriterState) scheduleBodyWrite() { - if s.timer == nil { - // We're not doing a delayed write (see - // getBodyWriterState), so just start the writing - // goroutine immediately. - go s.fn() - return - } - traceWait100Continue(s.cs.trace) - if s.timer.Stop() { - s.timer.Reset(s.delay) - } -} - // isConnectionCloseRequest reports whether req should use its own // connection for a single request and then close the connection. func isConnectionCloseRequest(req *http.Request) bool { diff --git a/vendor/golang.org/x/net/http2/writesched.go b/vendor/golang.org/x/net/http2/writesched.go index f24d2b1..c7cd001 100644 --- a/vendor/golang.org/x/net/http2/writesched.go +++ b/vendor/golang.org/x/net/http2/writesched.go @@ -32,7 +32,8 @@ type WriteScheduler interface { // Pop dequeues the next frame to write. Returns false if no frames can // be written. Frames with a given wr.StreamID() are Pop'd in the same - // order they are Push'd. No frames should be discarded except by CloseStream. + // order they are Push'd, except RST_STREAM frames. No frames should be + // discarded except by CloseStream. Pop() (wr FrameWriteRequest, ok bool) } @@ -52,6 +53,7 @@ type FrameWriteRequest struct { // stream is the stream on which this frame will be written. // nil for non-stream frames like PING and SETTINGS. + // nil for RST_STREAM streams, which use the StreamError.StreamID field instead. stream *stream // done, if non-nil, must be a buffered channel with space for diff --git a/vendor/golang.org/x/net/http2/writesched_random.go b/vendor/golang.org/x/net/http2/writesched_random.go index 9a7b9e5..f2e55e0 100644 --- a/vendor/golang.org/x/net/http2/writesched_random.go +++ b/vendor/golang.org/x/net/http2/writesched_random.go @@ -45,11 +45,11 @@ func (ws *randomWriteScheduler) AdjustStream(streamID uint32, priority PriorityP } func (ws *randomWriteScheduler) Push(wr FrameWriteRequest) { - id := wr.StreamID() - if id == 0 { + if wr.isControl() { ws.zero.push(wr) return } + id := wr.StreamID() q, ok := ws.sq[id] if !ok { q = ws.queuePool.get() @@ -59,7 +59,7 @@ func (ws *randomWriteScheduler) Push(wr FrameWriteRequest) { } func (ws *randomWriteScheduler) Pop() (FrameWriteRequest, bool) { - // Control frames first. + // Control and RST_STREAM frames first. if !ws.zero.empty() { return ws.zero.shift(), true } diff --git a/vendor/golang.org/x/net/idna/go118.go b/vendor/golang.org/x/net/idna/go118.go new file mode 100644 index 0000000..c5c4338 --- /dev/null +++ b/vendor/golang.org/x/net/idna/go118.go @@ -0,0 +1,14 @@ +// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. + +// Copyright 2021 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. + +//go:build go1.18 +// +build go1.18 + +package idna + +// Transitional processing is disabled by default in Go 1.18. +// https://golang.org/issue/47510 +const transitionalLookup = false diff --git a/vendor/golang.org/x/net/idna/idna10.0.0.go b/vendor/golang.org/x/net/idna/idna10.0.0.go index 5208ba6..64ccf85 100644 --- a/vendor/golang.org/x/net/idna/idna10.0.0.go +++ b/vendor/golang.org/x/net/idna/idna10.0.0.go @@ -59,10 +59,10 @@ type Option func(*options) // Transitional sets a Profile to use the Transitional mapping as defined in UTS // #46. This will cause, for example, "ß" to be mapped to "ss". Using the // transitional mapping provides a compromise between IDNA2003 and IDNA2008 -// compatibility. It is used by most browsers when resolving domain names. This +// compatibility. It is used by some browsers when resolving domain names. This // option is only meaningful if combined with MapForLookup. func Transitional(transitional bool) Option { - return func(o *options) { o.transitional = true } + return func(o *options) { o.transitional = transitional } } // VerifyDNSLength sets whether a Profile should fail if any of the IDN parts @@ -284,7 +284,7 @@ var ( punycode = &Profile{} lookup = &Profile{options{ - transitional: true, + transitional: transitionalLookup, useSTD3Rules: true, checkHyphens: true, checkJoiners: true, diff --git a/vendor/golang.org/x/net/idna/idna9.0.0.go b/vendor/golang.org/x/net/idna/idna9.0.0.go index 55f718f..aae6aac 100644 --- a/vendor/golang.org/x/net/idna/idna9.0.0.go +++ b/vendor/golang.org/x/net/idna/idna9.0.0.go @@ -58,10 +58,10 @@ type Option func(*options) // Transitional sets a Profile to use the Transitional mapping as defined in UTS // #46. This will cause, for example, "ß" to be mapped to "ss". Using the // transitional mapping provides a compromise between IDNA2003 and IDNA2008 -// compatibility. It is used by most browsers when resolving domain names. This +// compatibility. It is used by some browsers when resolving domain names. This // option is only meaningful if combined with MapForLookup. func Transitional(transitional bool) Option { - return func(o *options) { o.transitional = true } + return func(o *options) { o.transitional = transitional } } // VerifyDNSLength sets whether a Profile should fail if any of the IDN parts diff --git a/vendor/golang.org/x/net/idna/pre_go118.go b/vendor/golang.org/x/net/idna/pre_go118.go new file mode 100644 index 0000000..3aaccab --- /dev/null +++ b/vendor/golang.org/x/net/idna/pre_go118.go @@ -0,0 +1,12 @@ +// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. + +// Copyright 2021 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. + +//go:build !go1.18 +// +build !go1.18 + +package idna + +const transitionalLookup = true diff --git a/vendor/golang.org/x/net/idna/punycode.go b/vendor/golang.org/x/net/idna/punycode.go index 02c7d59..e8e3ac1 100644 --- a/vendor/golang.org/x/net/idna/punycode.go +++ b/vendor/golang.org/x/net/idna/punycode.go @@ -49,6 +49,7 @@ func decode(encoded string) (string, error) { } } i, n, bias := int32(0), initialN, initialBias + overflow := false for pos < len(encoded) { oldI, w := i, int32(1) for k := base; ; k += base { @@ -60,29 +61,32 @@ func decode(encoded string) (string, error) { return "", punyError(encoded) } pos++ - i += digit * w - if i < 0 { + i, overflow = madd(i, digit, w) + if overflow { return "", punyError(encoded) } t := k - bias - if t < tmin { + if k <= bias { t = tmin - } else if t > tmax { + } else if k >= bias+tmax { t = tmax } if digit < t { break } - w *= base - t - if w >= math.MaxInt32/base { + w, overflow = madd(0, w, base-t) + if overflow { return "", punyError(encoded) } } + if len(output) >= 1024 { + return "", punyError(encoded) + } x := int32(len(output) + 1) bias = adapt(i-oldI, x, oldI == 0) n += i / x i %= x - if n > utf8.MaxRune || len(output) >= 1024 { + if n < 0 || n > utf8.MaxRune { return "", punyError(encoded) } output = append(output, 0) @@ -115,6 +119,7 @@ func encode(prefix, s string) (string, error) { if b > 0 { output = append(output, '-') } + overflow := false for remaining != 0 { m := int32(0x7fffffff) for _, r := range s { @@ -122,8 +127,8 @@ func encode(prefix, s string) (string, error) { m = r } } - delta += (m - n) * (h + 1) - if delta < 0 { + delta, overflow = madd(delta, m-n, h+1) + if overflow { return "", punyError(s) } n = m @@ -141,9 +146,9 @@ func encode(prefix, s string) (string, error) { q := delta for k := base; ; k += base { t := k - bias - if t < tmin { + if k <= bias { t = tmin - } else if t > tmax { + } else if k >= bias+tmax { t = tmax } if q < t { @@ -164,6 +169,15 @@ func encode(prefix, s string) (string, error) { return string(output), nil } +// madd computes a + (b * c), detecting overflow. +func madd(a, b, c int32) (next int32, overflow bool) { + p := int64(b) * int64(c) + if p > math.MaxInt32-int64(a) { + return 0, true + } + return a + int32(p), false +} + func decodeDigit(x byte) (digit int32, ok bool) { switch { case '0' <= x && x <= '9': diff --git a/vendor/golang.org/x/term/codereview.cfg b/vendor/golang.org/x/term/codereview.cfg new file mode 100644 index 0000000..3f8b14b --- /dev/null +++ b/vendor/golang.org/x/term/codereview.cfg @@ -0,0 +1 @@ +issuerepo: golang/go diff --git a/vendor/golang.org/x/term/go.mod b/vendor/golang.org/x/term/go.mod index d45f528..edf0e5b 100644 --- a/vendor/golang.org/x/term/go.mod +++ b/vendor/golang.org/x/term/go.mod @@ -1,5 +1,5 @@ module golang.org/x/term -go 1.11 +go 1.17 -require golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 +require golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 diff --git a/vendor/golang.org/x/term/go.sum b/vendor/golang.org/x/term/go.sum index de9e09c..ff13213 100644 --- a/vendor/golang.org/x/term/go.sum +++ b/vendor/golang.org/x/term/go.sum @@ -1,2 +1,2 @@ -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/vendor/golang.org/x/term/term.go b/vendor/golang.org/x/term/term.go index 2a4ccf8..d592708 100644 --- a/vendor/golang.org/x/term/term.go +++ b/vendor/golang.org/x/term/term.go @@ -7,11 +7,13 @@ // // Putting a terminal into raw mode is the most common requirement: // -// oldState, err := term.MakeRaw(0) +// oldState, err := term.MakeRaw(int(os.Stdin.Fd())) // if err != nil { // panic(err) // } -// defer term.Restore(0, oldState) +// defer term.Restore(int(os.Stdin.Fd()), oldState) +// +// Note that on non-Unix systems os.Stdin.Fd() may not be 0. package term // State contains the state of a terminal. diff --git a/vendor/golang.org/x/term/term_solaris.go b/vendor/golang.org/x/term/term_solaris.go deleted file mode 100644 index b9da297..0000000 --- a/vendor/golang.org/x/term/term_solaris.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package term - -import ( - "io" - "syscall" - - "golang.org/x/sys/unix" -) - -// State contains the state of a terminal. -type state struct { - termios unix.Termios -} - -func isTerminal(fd int) bool { - _, err := unix.IoctlGetTermio(fd, unix.TCGETA) - return err == nil -} - -func readPassword(fd int) ([]byte, error) { - // see also: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libast/common/uwin/getpass.c - val, err := unix.IoctlGetTermios(fd, unix.TCGETS) - if err != nil { - return nil, err - } - oldState := *val - - newState := oldState - newState.Lflag &^= syscall.ECHO - newState.Lflag |= syscall.ICANON | syscall.ISIG - newState.Iflag |= syscall.ICRNL - err = unix.IoctlSetTermios(fd, unix.TCSETS, &newState) - if err != nil { - return nil, err - } - - defer unix.IoctlSetTermios(fd, unix.TCSETS, &oldState) - - var buf [16]byte - var ret []byte - for { - n, err := syscall.Read(fd, buf[:]) - if err != nil { - return nil, err - } - if n == 0 { - if len(ret) == 0 { - return nil, io.EOF - } - break - } - if buf[n-1] == '\n' { - n-- - } - ret = append(ret, buf[:n]...) - if n < len(buf) { - break - } - } - - return ret, nil -} - -func makeRaw(fd int) (*State, error) { - // see http://cr.illumos.org/~webrev/andy_js/1060/ - termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) - if err != nil { - return nil, err - } - - oldState := State{state{termios: *termios}} - - termios.Iflag &^= unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON - termios.Oflag &^= unix.OPOST - termios.Lflag &^= unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN - termios.Cflag &^= unix.CSIZE | unix.PARENB - termios.Cflag |= unix.CS8 - termios.Cc[unix.VMIN] = 1 - termios.Cc[unix.VTIME] = 0 - - if err := unix.IoctlSetTermios(fd, unix.TCSETS, termios); err != nil { - return nil, err - } - - return &oldState, nil -} - -func restore(fd int, oldState *State) error { - return unix.IoctlSetTermios(fd, unix.TCSETS, &oldState.termios) -} - -func getState(fd int) (*State, error) { - termios, err := unix.IoctlGetTermios(fd, unix.TCGETS) - if err != nil { - return nil, err - } - - return &State{state{termios: *termios}}, nil -} - -func getSize(fd int) (width, height int, err error) { - ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ) - if err != nil { - return 0, 0, err - } - return int(ws.Col), int(ws.Row), nil -} diff --git a/vendor/golang.org/x/term/term_unix.go b/vendor/golang.org/x/term/term_unix.go index 6849b6e..a4e31ab 100644 --- a/vendor/golang.org/x/term/term_unix.go +++ b/vendor/golang.org/x/term/term_unix.go @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || zos -// +build aix darwin dragonfly freebsd linux netbsd openbsd zos +//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos +// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris zos package term diff --git a/vendor/golang.org/x/term/term_unix_aix.go b/vendor/golang.org/x/term/term_unix_aix.go deleted file mode 100644 index 2d5efd2..0000000 --- a/vendor/golang.org/x/term/term_unix_aix.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package term - -import "golang.org/x/sys/unix" - -const ioctlReadTermios = unix.TCGETS -const ioctlWriteTermios = unix.TCSETS diff --git a/vendor/golang.org/x/term/term_unix_linux.go b/vendor/golang.org/x/term/term_unix_linux.go deleted file mode 100644 index 2d5efd2..0000000 --- a/vendor/golang.org/x/term/term_unix_linux.go +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package term - -import "golang.org/x/sys/unix" - -const ioctlReadTermios = unix.TCGETS -const ioctlWriteTermios = unix.TCSETS diff --git a/vendor/golang.org/x/term/term_unix_zos.go b/vendor/golang.org/x/term/term_unix_other.go similarity index 63% rename from vendor/golang.org/x/term/term_unix_zos.go rename to vendor/golang.org/x/term/term_unix_other.go index b85ab89..1e8955c 100644 --- a/vendor/golang.org/x/term/term_unix_zos.go +++ b/vendor/golang.org/x/term/term_unix_other.go @@ -1,7 +1,10 @@ -// Copyright 2020 The Go Authors. All rights reserved. +// Copyright 2021 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. +//go:build aix || linux || solaris || zos +// +build aix linux solaris zos + package term import "golang.org/x/sys/unix" diff --git a/vendor/golang.org/x/text/internal/language/language.go b/vendor/golang.org/x/text/internal/language/language.go index f41aedc..6105bc7 100644 --- a/vendor/golang.org/x/text/internal/language/language.go +++ b/vendor/golang.org/x/text/internal/language/language.go @@ -251,6 +251,13 @@ func (t Tag) Parent() Tag { // ParseExtension parses s as an extension and returns it on success. func ParseExtension(s string) (ext string, err error) { + defer func() { + if recover() != nil { + ext = "" + err = ErrSyntax + } + }() + scan := makeScannerString(s) var end int if n := len(scan.token); n != 1 { @@ -461,7 +468,14 @@ func (t Tag) findTypeForKey(key string) (start, sep, end int, hasExt bool) { // ParseBase parses a 2- or 3-letter ISO 639 code. // It returns a ValueError if s is a well-formed but unknown language identifier // or another error if another error occurred. -func ParseBase(s string) (Language, error) { +func ParseBase(s string) (l Language, err error) { + defer func() { + if recover() != nil { + l = 0 + err = ErrSyntax + } + }() + if n := len(s); n < 2 || 3 < n { return 0, ErrSyntax } @@ -472,7 +486,14 @@ func ParseBase(s string) (Language, error) { // ParseScript parses a 4-letter ISO 15924 code. // It returns a ValueError if s is a well-formed but unknown script identifier // or another error if another error occurred. -func ParseScript(s string) (Script, error) { +func ParseScript(s string) (scr Script, err error) { + defer func() { + if recover() != nil { + scr = 0 + err = ErrSyntax + } + }() + if len(s) != 4 { return 0, ErrSyntax } @@ -489,7 +510,14 @@ func EncodeM49(r int) (Region, error) { // ParseRegion parses a 2- or 3-letter ISO 3166-1 or a UN M.49 code. // It returns a ValueError if s is a well-formed but unknown region identifier // or another error if another error occurred. -func ParseRegion(s string) (Region, error) { +func ParseRegion(s string) (r Region, err error) { + defer func() { + if recover() != nil { + r = 0 + err = ErrSyntax + } + }() + if n := len(s); n < 2 || 3 < n { return 0, ErrSyntax } @@ -578,7 +606,14 @@ type Variant struct { // ParseVariant parses and returns a Variant. An error is returned if s is not // a valid variant. -func ParseVariant(s string) (Variant, error) { +func ParseVariant(s string) (v Variant, err error) { + defer func() { + if recover() != nil { + v = Variant{} + err = ErrSyntax + } + }() + s = strings.ToLower(s) if id, ok := variantIndex[s]; ok { return Variant{id, s}, nil diff --git a/vendor/golang.org/x/text/internal/language/parse.go b/vendor/golang.org/x/text/internal/language/parse.go index c696fd0..47ee0fe 100644 --- a/vendor/golang.org/x/text/internal/language/parse.go +++ b/vendor/golang.org/x/text/internal/language/parse.go @@ -232,6 +232,13 @@ func Parse(s string) (t Tag, err error) { if s == "" { return Und, ErrSyntax } + defer func() { + if recover() != nil { + t = Und + err = ErrSyntax + return + } + }() if len(s) <= maxAltTaglen { b := [maxAltTaglen]byte{} for i, c := range s { diff --git a/vendor/golang.org/x/text/language/parse.go b/vendor/golang.org/x/text/language/parse.go index 11acfd8..59b0410 100644 --- a/vendor/golang.org/x/text/language/parse.go +++ b/vendor/golang.org/x/text/language/parse.go @@ -43,6 +43,13 @@ func Parse(s string) (t Tag, err error) { // https://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers. // The resulting tag is canonicalized using the canonicalization type c. func (c CanonType) Parse(s string) (t Tag, err error) { + defer func() { + if recover() != nil { + t = Tag{} + err = language.ErrSyntax + } + }() + tt, err := language.Parse(s) if err != nil { return makeTag(tt), err @@ -79,6 +86,13 @@ func Compose(part ...interface{}) (t Tag, err error) { // tag is returned after canonicalizing using CanonType c. If one or more errors // are encountered, one of the errors is returned. func (c CanonType) Compose(part ...interface{}) (t Tag, err error) { + defer func() { + if recover() != nil { + t = Tag{} + err = language.ErrSyntax + } + }() + var b language.Builder if err = update(&b, part...); err != nil { return und, err @@ -142,6 +156,14 @@ var errInvalidWeight = errors.New("ParseAcceptLanguage: invalid weight") // Tags with a weight of zero will be dropped. An error will be returned if the // input could not be parsed. func ParseAcceptLanguage(s string) (tag []Tag, q []float32, err error) { + defer func() { + if recover() != nil { + tag = nil + q = nil + err = language.ErrSyntax + } + }() + var entry string for s != "" { if entry, s = split(s, ','); entry == "" { diff --git a/vendor/modules.txt b/vendor/modules.txt index de7fad3..b619c99 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -13,7 +13,7 @@ github.com/beorn7/perks/quantile github.com/blang/semver # github.com/cespare/xxhash/v2 v2.1.1 github.com/cespare/xxhash/v2 -# github.com/civo/civogo v0.2.70 +# github.com/civo/civogo v0.2.93 ## explicit github.com/civo/civogo github.com/civo/civogo/utils @@ -31,7 +31,6 @@ github.com/davecgh/go-spew/spew github.com/emicklei/go-restful github.com/emicklei/go-restful/log # github.com/evanphx/json-patch v4.11.0+incompatible -## explicit github.com/evanphx/json-patch # github.com/go-logr/logr v0.4.0 github.com/go-logr/logr @@ -70,18 +69,15 @@ github.com/google/gofuzz ## explicit github.com/google/uuid # github.com/googleapis/gnostic v0.5.5 => github.com/googleapis/gnostic v0.4.1 -## explicit github.com/googleapis/gnostic/compiler github.com/googleapis/gnostic/extensions github.com/googleapis/gnostic/openapiv2 # github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/grpc-ecosystem/go-grpc-prometheus # github.com/hashicorp/golang-lru v0.5.4 -## explicit github.com/hashicorp/golang-lru github.com/hashicorp/golang-lru/simplelru # github.com/imdario/mergo v0.3.12 -## explicit github.com/imdario/mergo # github.com/inconshreveable/mousetrap v1.0.0 github.com/inconshreveable/mousetrap @@ -105,7 +101,7 @@ github.com/modern-go/concurrent github.com/modern-go/reflect2 # github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/munnerz/goautoneg -# github.com/onsi/gomega v1.18.0 +# github.com/onsi/gomega v1.19.0 ## explicit github.com/onsi/gomega github.com/onsi/gomega/format @@ -120,7 +116,6 @@ github.com/onsi/gomega/types # github.com/pkg/errors v0.9.1 github.com/pkg/errors # github.com/prometheus/client_golang v1.11.0 -## explicit github.com/prometheus/client_golang/prometheus github.com/prometheus/client_golang/prometheus/internal github.com/prometheus/client_golang/prometheus/promhttp @@ -170,7 +165,6 @@ go.uber.org/atomic # go.uber.org/multierr v1.6.0 go.uber.org/multierr # go.uber.org/zap v1.17.0 -## explicit go.uber.org/zap go.uber.org/zap/buffer go.uber.org/zap/internal/bufferpool @@ -184,8 +178,7 @@ golang.org/x/crypto/internal/subtle golang.org/x/crypto/nacl/secretbox golang.org/x/crypto/poly1305 golang.org/x/crypto/salsa20/salsa -# golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b -## explicit +# golang.org/x/net v0.0.0-20220225172249-27dd8689420f golang.org/x/net/context golang.org/x/net/context/ctxhttp golang.org/x/net/html @@ -209,9 +202,9 @@ golang.org/x/sys/internal/unsafeheader golang.org/x/sys/plan9 golang.org/x/sys/unix golang.org/x/sys/windows -# golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d +# golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 golang.org/x/term -# golang.org/x/text v0.3.6 +# golang.org/x/text v0.3.7 golang.org/x/text/encoding golang.org/x/text/encoding/charmap golang.org/x/text/encoding/htmlindex @@ -236,7 +229,6 @@ golang.org/x/text/width # golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba golang.org/x/time/rate # google.golang.org/appengine v1.6.7 -## explicit google.golang.org/appengine/internal google.golang.org/appengine/internal/base google.golang.org/appengine/internal/datastore @@ -880,7 +872,6 @@ k8s.io/kube-openapi/pkg/schemaconv k8s.io/kube-openapi/pkg/util k8s.io/kube-openapi/pkg/util/proto # k8s.io/utils v0.0.0-20210527160623-6fdb442a123b -## explicit k8s.io/utils/buffer k8s.io/utils/integer k8s.io/utils/net