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