diff --git a/dev-images.json b/dev-images.json index e7e12b378..d70999de9 100644 --- a/dev-images.json +++ b/dev-images.json @@ -5,6 +5,7 @@ "azure-cluster-api-controllers": "quay.io/ademicev/cluster-api-provider-azure:latest", "gcp-cluster-api-controllers": "quay.io/ademicev/cluster-api-provider-gcp:latest", "ibmcloud-cluster-api-controllers": "quay.io/kabhat/ibmcloud-cluster-api-controllers:dev", + "openstack-cluster-api-controllers": "quay.io/stephenfin/cluster-api-provider-openstack:latest", "vsphere-cluster-api-controllers": "quay.io/dodvarka/cluster-api-provider-vsphere:latest", "kube-rbac-proxy": "gcr.io/kubebuilder/kube-rbac-proxy:v0.8.0" } diff --git a/e2e/e2e_common.go b/e2e/e2e_common.go index e208d6ee3..1845db47a 100644 --- a/e2e/e2e_common.go +++ b/e2e/e2e_common.go @@ -17,6 +17,7 @@ import ( azurev1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" gcpv1 "sigs.k8s.io/cluster-api-provider-gcp/api/v1beta1" ibmpowervsv1 "sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2" + openstackv1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" vspherev1 "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" ctrl "sigs.k8s.io/controller-runtime" @@ -48,6 +49,7 @@ func init() { utilruntime.Must(clusterv1.AddToScheme(scheme.Scheme)) utilruntime.Must(mapiv1beta1.AddToScheme(scheme.Scheme)) utilruntime.Must(ibmpowervsv1.AddToScheme(scheme.Scheme)) + utilruntime.Must(openstackv1.AddToScheme(scheme.Scheme)) utilruntime.Must(vspherev1.AddToScheme(scheme.Scheme)) utilruntime.Must(metal3v1.AddToScheme(scheme.Scheme)) utilruntime.Must(bmov1alpha1.AddToScheme(scheme.Scheme)) diff --git a/e2e/go.mod b/e2e/go.mod index f0379dbe7..7eedb67b2 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -21,6 +21,7 @@ require ( sigs.k8s.io/cluster-api-provider-azure v1.20.2 sigs.k8s.io/cluster-api-provider-gcp v1.10.0 sigs.k8s.io/cluster-api-provider-ibmcloud v0.11.0 + sigs.k8s.io/cluster-api-provider-openstack v0.12.4 sigs.k8s.io/cluster-api-provider-vsphere v1.13.0 sigs.k8s.io/controller-runtime v0.20.4 sigs.k8s.io/yaml v1.4.0 @@ -63,6 +64,7 @@ require ( github.com/google/gnostic-models v0.6.9 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gophercloud/gophercloud/v2 v2.9.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index d7f123600..8bdb9c151 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -117,6 +117,8 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gophercloud/gophercloud/v2 v2.9.0 h1:Y9OMrwKF9EDERcHFSOTpf/6XGoAI0yOxmsLmQki4LPM= +github.com/gophercloud/gophercloud/v2 v2.9.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= @@ -373,6 +375,8 @@ sigs.k8s.io/cluster-api-provider-gcp v1.10.0 h1:gngUxo9bz8l+otpdw9v3ULEXwAvijyqi sigs.k8s.io/cluster-api-provider-gcp v1.10.0/go.mod h1:VkmOqBgi3Jj+VR+YTZEHAlMMmyjqcKl4lXhpLsYnYok= sigs.k8s.io/cluster-api-provider-ibmcloud v0.11.0 h1:aunR3nnDzQ5x1Qj1hJbR3Xq4SCZth4XyTWAyUCN46kE= sigs.k8s.io/cluster-api-provider-ibmcloud v0.11.0/go.mod h1:9yPLATyiqLx4crMzbX11YQU+GuR5txjtiCt8z9sxDfM= +sigs.k8s.io/cluster-api-provider-openstack v0.12.4 h1:zTg4UrFbK8GwUm+7983mm0Omz/w4BLB+VSmbELqGimc= +sigs.k8s.io/cluster-api-provider-openstack v0.12.4/go.mod h1:tYFVtlu1NqU9H5edUaz2QkXDhucn6mAY9mHeK9pEKzU= sigs.k8s.io/cluster-api-provider-vsphere v1.13.0 h1:tvJX0p/LfBvOvF1EWNu4+9EJALBTAHU1US7Z1SX/24Q= sigs.k8s.io/cluster-api-provider-vsphere v1.13.0/go.mod h1:TgNq4vlGPmUhCmzT2hOptOZuNbyj0JfOXFLufr91tTY= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= diff --git a/e2e/openstack_test.go b/e2e/openstack_test.go new file mode 100644 index 000000000..780553609 --- /dev/null +++ b/e2e/openstack_test.go @@ -0,0 +1,167 @@ +package e2e + +import ( + "context" + + "github.com/onsi/gomega/format" + configv1 "github.com/openshift/api/config/v1" + mapiv1alpha1 "github.com/openshift/api/machine/v1alpha1" + mapiv1beta1 "github.com/openshift/api/machine/v1beta1" + "github.com/openshift/cluster-capi-operator/e2e/framework" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + yaml "sigs.k8s.io/yaml" + + openstackv1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + openStackMachineTemplateName = "openstack-machine-template" +) + +var _ = Describe("Cluster API OpenStack MachineSet", Ordered, func() { + var ( + machineSet *clusterv1.MachineSet + mapiMachineSpec *mapiv1alpha1.OpenstackProviderSpec + openStackMachineTemplate *openstackv1.OpenStackMachineTemplate + ) + + BeforeAll(func() { + if platform != configv1.OpenStackPlatformType { + Skip("Skipping OpenStack E2E tests") + } + mapiMachineSpec = getOpenStackMAPIProviderSpec(cl) + }) + + AfterEach(func() { + if platform != configv1.OpenStackPlatformType { + // Because AfterEach always runs, even when tests are skipped, we have to + // explicitly skip it here for other platforms. + Skip("Skipping OpenStack E2E tests") + } + + framework.DeleteMachineSets(ctx, cl, machineSet) + framework.WaitForMachineSetsDeleted(cl, machineSet) + framework.DeleteObjects(ctx, cl, openStackMachineTemplate) + }) + + It("should be able to run a machine with implicit cluster default network", func() { + openStackMachineTemplate = createOpenStackMachineTemplate(ctx, cl, mapiMachineSpec) + + machineSet = framework.CreateMachineSet(ctx, cl, framework.NewMachineSetParams( + "openstack-machineset", + clusterName, + "", + 1, + corev1.ObjectReference{ + Kind: "OpenStackMachineTemplate", + APIVersion: infraAPIVersion, + Name: openStackMachineTemplate.Name, + }, + "worker-user-data", + )) + + framework.WaitForMachineSet(cl, machineSet.Name, machineSet.Namespace) + }) +}) + +func getOpenStackMAPIProviderSpec(cl client.Client) *mapiv1alpha1.OpenstackProviderSpec { + machineSetList := &mapiv1beta1.MachineSetList{} + Eventually(cl.List(ctx, machineSetList, client.InNamespace(framework.MAPINamespace))).Should(Succeed()) + + Expect(machineSetList.Items).ToNot(HaveLen(0), "No MachineSets found in namespace %s", framework.MAPINamespace) + machineSet := machineSetList.Items[0] + Expect(machineSet.Spec.Template.Spec.ProviderSpec.Value).ToNot(BeNil()) + + providerSpec := &mapiv1alpha1.OpenstackProviderSpec{} + Expect(yaml.Unmarshal(machineSet.Spec.Template.Spec.ProviderSpec.Value.Raw, providerSpec)).To(Succeed()) + + return providerSpec +} + +func createOpenStackMachineTemplate(ctx context.Context, cl client.Client, mapiProviderSpec *mapiv1alpha1.OpenstackProviderSpec) *openstackv1.OpenStackMachineTemplate { + By("Creating OpenStack machine template") + + Expect(mapiProviderSpec).ToNot(BeNil()) + Expect(mapiProviderSpec.Flavor).ToNot(BeEmpty()) + // NOTE(stephenfin): Installer does not populate ps.Image when ps.RootVolume is set and will + // instead populate ps.RootVolume.SourceUUID. Moreover, according to the ClusterOSImage option + // definition this is always the name of the image and never the UUID. We should allow UUID + // at some point and this will need an update. + if mapiProviderSpec.RootVolume != nil { + Expect(mapiProviderSpec.RootVolume.SourceUUID).ToNot(BeEmpty()) + } else { + Expect(mapiProviderSpec.Image).ToNot(BeEmpty()) + } + Expect(len(mapiProviderSpec.Networks)).To(BeNumerically(">", 0)) + Expect(len(mapiProviderSpec.Networks[0].Subnets)).To(BeNumerically(">", 0)) + Expect(mapiProviderSpec.Tags).ToNot(BeNil()) + Expect(len(mapiProviderSpec.Tags)).To(BeNumerically(">", 0)) + + var image openstackv1.ImageParam + var rootVolume *openstackv1.RootVolume + + if mapiProviderSpec.RootVolume != nil { + rootVolume = &openstackv1.RootVolume{ + SizeGiB: mapiProviderSpec.RootVolume.Size, + BlockDeviceVolume: openstackv1.BlockDeviceVolume{ + Type: mapiProviderSpec.RootVolume.VolumeType, + AvailabilityZone: &openstackv1.VolumeAvailabilityZone{ + From: openstackv1.VolumeAZFromName, + Name: ptr.To(openstackv1.VolumeAZName(mapiProviderSpec.RootVolume.Zone)), + }, + }, + } + image.ID = ptr.To(mapiProviderSpec.RootVolume.SourceUUID) + } else { + image.Filter = &openstackv1.ImageFilter{Name: &mapiProviderSpec.Image} + } + + // NOTE(stephenfin): We intentionally ignore additional security for now. + var securityGroupParam openstackv1.SecurityGroupParam + Expect(len(mapiProviderSpec.SecurityGroups)).To(BeNumerically(">", 0)) + securityGroup := mapiProviderSpec.SecurityGroups[0] + if securityGroup.UUID != "" { + securityGroupParam = openstackv1.SecurityGroupParam{ID: &securityGroup.UUID} + } else { + securityGroupParam = openstackv1.SecurityGroupParam{Filter: &openstackv1.SecurityGroupFilter{Name: securityGroup.Name}} + } + securityGroups := []openstackv1.SecurityGroupParam{ + securityGroupParam, + } + + // We intentionally omit ports so the machine will default its network + // from the OpenStackCluster created by the infracluster controller. + openStackMachineSpec := openstackv1.OpenStackMachineSpec{ + Flavor: ptr.To(mapiProviderSpec.Flavor), + IdentityRef: &openstackv1.OpenStackIdentityReference{ + CloudName: "openstack", + Name: "openstack-cloud-credentials", + }, + Image: image, + RootVolume: rootVolume, + SecurityGroups: securityGroups, + } + + openStackMachineTemplate := &openstackv1.OpenStackMachineTemplate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: openStackMachineTemplateName + "-", + Namespace: framework.CAPINamespace, + }, + Spec: openstackv1.OpenStackMachineTemplateSpec{ + Template: openstackv1.OpenStackMachineTemplateResource{ + Spec: openStackMachineSpec, + }, + }, + } + + Expect(cl.Create(ctx, openStackMachineTemplate)).To(Succeed(), format.Object(openStackMachineTemplate, 1)) + + return openStackMachineTemplate +} diff --git a/go.mod b/go.mod index 0a2e3c219..9370c91ce 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/golangci/golangci-lint v1.64.8 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 + github.com/gophercloud/gophercloud/v2 v2.9.0 github.com/klauspost/compress v1.18.0 github.com/metal3-io/cluster-api-provider-metal3/api v1.10.1 github.com/onsi/ginkgo/v2 v2.23.4 @@ -134,6 +135,7 @@ require ( github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.12.1 // indirect + github.com/gofrs/uuid/v5 v5.3.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/mock v1.7.0-rc.1 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect @@ -147,7 +149,7 @@ require ( github.com/google/cel-go v0.23.2 // indirect github.com/google/gnostic-models v0.6.9 // indirect github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect - github.com/gophercloud/gophercloud/v2 v2.7.0 // indirect + github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect @@ -273,6 +275,7 @@ require ( go.opentelemetry.io/otel/trace v1.36.0 // indirect go.opentelemetry.io/proto/otlp v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect + go.uber.org/mock v0.5.2 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/crypto v0.39.0 // indirect diff --git a/go.sum b/go.sum index 74bd4937d..cdccb1fb0 100644 --- a/go.sum +++ b/go.sum @@ -225,6 +225,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= +github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= @@ -271,8 +273,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gophercloud/gophercloud/v2 v2.7.0 h1:o0m4kgVcPgHlcXiWAjoVxGd8QCmvM5VU+YM71pFbn0E= -github.com/gophercloud/gophercloud/v2 v2.7.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +github.com/gophercloud/gophercloud/v2 v2.9.0 h1:Y9OMrwKF9EDERcHFSOTpf/6XGoAI0yOxmsLmQki4LPM= +github.com/gophercloud/gophercloud/v2 v2.9.0/go.mod h1:Ki/ILhYZr/5EPebrPL9Ej+tUg4lqx71/YH2JWVeU+Qk= +github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 h1:N65GYmx5LrMeYdeXcxMESDU+2pDyAOXlFNlHl7siUwM= +github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26/go.mod h1:7SHUbtoiSYINNKgAVxse+PMhIio05IK7shHy8DVRaN0= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= github.com/gordonklaus/ineffassign v0.1.0/go.mod h1:Qcp2HIAYhR7mNUVSIxZww3Guk4it82ghYcEXIAk+QT0= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= @@ -620,6 +624,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= diff --git a/hack/e2e-openstack.sh b/hack/e2e-openstack.sh index b195d4371..56fb31dc3 100755 --- a/hack/e2e-openstack.sh +++ b/hack/e2e-openstack.sh @@ -1,14 +1,7 @@ -#!/bin/bash +#!/usr/bin/env bash -set -euo pipefail +# TODO(stephenfin): This is legacy from when the OpenStack e2e tests lived +# elsewhere. We should remove this script and update the CI jobs to call the +# Makefile target like everyone else -echo "Running e2e-openstack.sh" - -unset GOFLAGS -tmp="$(mktemp -d)" - -echo "cloning github.com/openshift/cluster-api-provider-openstack at branch '$PULL_BASE_REF'" -git clone --single-branch --branch="$PULL_BASE_REF" --depth=1 "https://github.com/openshift/cluster-api-provider-openstack.git" "$tmp" - -echo "running cluster-api-provider-openstack's: make e2e" -exec make -C "$tmp/openshift" e2e +exec make e2e diff --git a/manifests/0000_30_cluster-api_01_credentials-request.yaml b/manifests/0000_30_cluster-api_01_credentials-request.yaml index 08b6b87d3..559dd4d20 100644 --- a/manifests/0000_30_cluster-api_01_credentials-request.yaml +++ b/manifests/0000_30_cluster-api_01_credentials-request.yaml @@ -221,6 +221,24 @@ spec: --- apiVersion: cloudcredential.openshift.io/v1 kind: CredentialsRequest +metadata: + name: openshift-cluster-api-openstack + namespace: openshift-cloud-credential-operator + annotations: + capability.openshift.io/name: CloudCredential + exclude.release.openshift.io/internal-openshift-hosted: "true" + include.release.openshift.io/self-managed-high-availability: "true" + release.openshift.io/feature-set: CustomNoUpgrade,TechPreviewNoUpgrade +spec: + providerSpec: + apiVersion: cloudcredential.openshift.io/v1 + kind: OpenStackProviderSpec + secretRef: + name: openstack-cloud-credentials + namespace: openshift-cluster-api +--- +apiVersion: cloudcredential.openshift.io/v1 +kind: CredentialsRequest metadata: name: openshift-cluster-api-powervs namespace: openshift-cloud-credential-operator diff --git a/manifests/0000_30_cluster-api_01_images.configmap.yaml b/manifests/0000_30_cluster-api_01_images.configmap.yaml index 43d8da8ca..4ef1f1287 100644 --- a/manifests/0000_30_cluster-api_01_images.configmap.yaml +++ b/manifests/0000_30_cluster-api_01_images.configmap.yaml @@ -18,6 +18,7 @@ data: "azure-cluster-api-controllers": "registry.ci.openshift.org/openshift:azure-cluster-api-controllers", "gcp-cluster-api-controllers": "registry.ci.openshift.org/openshift:gcp-cluster-api-controllers", "ibmcloud-cluster-api-controllers": "registry.ci.openshift.org/openshift:ibmcloud-cluster-api-controllers", + "openstack-cluster-api-controllers": "registry.ci.openshift.org/openshift:openstack-cluster-api-controllers", "vsphere-cluster-api-controllers": "registry.ci.openshift.org/openshift:vsphere-cluster-api-controllers", "baremetal-cluster-api-controllers": "registry.ci.openshift.org/openshift:baremetal-cluster-api-controllers", "kube-rbac-proxy": "registry.ci.openshift.org/openshift:kube-rbac-proxy" diff --git a/manifests/image-references b/manifests/image-references index d6e65e2db..a9d7cedf7 100644 --- a/manifests/image-references +++ b/manifests/image-references @@ -38,6 +38,10 @@ spec: from: kind: DockerImage name: registry.ci.openshift.org/openshift:ibmcloud-cluster-api-controllers + - name: openstack-cluster-api-controllers + from: + kind: DockerImage + name: registry.ci.openshift.org/openshift:openstack-cluster-api-controllers - name: vsphere-cluster-api-controllers from: kind: DockerImage diff --git a/pkg/controllers/capiinstaller/component_customizer.go b/pkg/controllers/capiinstaller/component_customizer.go index e9160d37e..0127372b5 100644 --- a/pkg/controllers/capiinstaller/component_customizer.go +++ b/pkg/controllers/capiinstaller/component_customizer.go @@ -32,6 +32,8 @@ func providerNameToImageKey(name string) string { return "baremetal-cluster-api-controllers" case "ibmcloud": return "ibmcloud-cluster-api-controllers" + case "openstack": + return "openstack-cluster-api-controllers" case "cluster-api": return "cluster-capi-controllers" default: diff --git a/pkg/controllers/infracluster/infracluster_controller.go b/pkg/controllers/infracluster/infracluster_controller.go index 3b2cf4a6d..eac4f5781 100644 --- a/pkg/controllers/infracluster/infracluster_controller.go +++ b/pkg/controllers/infracluster/infracluster_controller.go @@ -34,8 +34,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" - openstackv1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" - configv1 "github.com/openshift/api/config/v1" mapiv1 "github.com/openshift/api/machine/v1" mapiv1beta1 "github.com/openshift/api/machine/v1beta1" @@ -210,7 +208,7 @@ func (r *InfraClusterController) ensureInfraCluster(ctx context.Context, log log infraCluster, err = r.ensureIBMPowerVSCluster(ctx, log) if err != nil { - return nil, fmt.Errorf("error ensuring IBMPowerVSCluster: %w", err) + return nil, fmt.Errorf("error getting InfraCluster object: %w", err) } case configv1.VSpherePlatformType: var err error @@ -227,12 +225,12 @@ func (r *InfraClusterController) ensureInfraCluster(ctx context.Context, log log return nil, fmt.Errorf("error getting InfraCluster object: %w", err) } case configv1.OpenStackPlatformType: - openstackCluster := &openstackv1.OpenStackCluster{} - if err := r.Get(ctx, client.ObjectKey{Namespace: r.CAPINamespace, Name: r.Infra.Status.InfrastructureName}, openstackCluster); err != nil && !kerrors.IsNotFound(err) { + var err error + + infraCluster, err = r.ensureOpenStackCluster(ctx, log) + if err != nil { return nil, fmt.Errorf("error getting InfraCluster object: %w", err) } - - infraCluster = openstackCluster default: return nil, errPlatformNotSupported } diff --git a/pkg/controllers/infracluster/openstack.go b/pkg/controllers/infracluster/openstack.go new file mode 100644 index 000000000..f77a2ccb3 --- /dev/null +++ b/pkg/controllers/infracluster/openstack.go @@ -0,0 +1,313 @@ +/* +Copyright 2025 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package infracluster + +import ( + "context" + "errors" + "fmt" + "net" + "slices" + "strings" + + "github.com/go-logr/logr" + configv1 "github.com/openshift/api/config/v1" + mapiv1beta1 "github.com/openshift/api/machine/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + "k8s.io/utils/ptr" + clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" + openstackv1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + openstackclients "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients" + openstackscope "sigs.k8s.io/cluster-api-provider-openstack/pkg/scope" +) + +var ( + errUnsupportedOpenStackLoadBalancerType = errors.New("unsupported load balancer type for OpenStack") + errOpenStackNoAPIServerInternalIPs = errors.New("no APIServerInternalIPs available") + errOpenStackNoDefaultRouter = errors.New("unable to determine default router from control plane machines") + errOpenStackNoDefaultSubnet = errors.New("unable to determine default subnet from control plane machines") + errOpenStackNoControlPlaneMachines = errors.New("no control plane machines found") +) + +// ensureOpenStackCluster ensures the OpenStackCluster object exists. +// +//nolint:funlen +func (r *InfraClusterController) ensureOpenStackCluster(ctx context.Context, log logr.Logger) (client.Object, error) { + target := &openstackv1.OpenStackCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.Infra.Status.InfrastructureName, + Namespace: r.CAPINamespace, + }, + } + + // Checking whether InfraCluster object exists. If it doesn't, create it. + + if err := r.Get(ctx, client.ObjectKeyFromObject(target), target); err != nil && !apierrors.IsNotFound(err) { + return nil, fmt.Errorf("failed to get InfraCluster: %w", err) + } else if err == nil { + log.V(4).Info("OpenStackCluster already exists") + return target, nil + } + + log.Info(fmt.Sprintf("OpenStackCluster %s does not exist, creating it", klog.KObj(target))) + + // FIXME(stephenfin): Other providers use the below but not us. Why? + // + // apiURL, err := url.Parse(r.Infra.Status.APIServerInternalURL) + // if err != nil { + // return nil, fmt.Errorf("failed to parse apiURL: %w", err) + // } + // + // port, err := strconv.ParseInt(apiURL.Port(), 10, 32) + // if err != nil { + // return nil, fmt.Errorf("failed to parse apiURL port: %w", err) + // } + + if r.Infra.Status.PlatformStatus == nil { + return nil, errInvalidPlatformStatus + } + + // Perform some platform-specific validation + + platformStatus := r.Infra.Status.PlatformStatus.OpenStack + + if platformStatus.LoadBalancer.Type != configv1.LoadBalancerTypeOpenShiftManagedDefault { + return nil, fmt.Errorf("%w: load balancer type %s not supported", + errUnsupportedOpenStackLoadBalancerType, platformStatus.LoadBalancer.Type) + } + + if len(platformStatus.APIServerInternalIPs) == 0 { + return nil, errOpenStackNoAPIServerInternalIPs + } + + target = &openstackv1.OpenStackCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: r.Infra.Status.InfrastructureName, + Namespace: r.CAPINamespace, + // NOTE(stephenfin): Other providers set the ManagedBy annotation here so that CAPI + // infra providers (CAPO here) ignore the InfraCluster object. However, we need CAPO + // to populate the .Status field for us so we *do not* set the annotation here. + }, + Spec: openstackv1.OpenStackClusterSpec{ + ControlPlaneEndpoint: &clusterv1.APIEndpoint{ + // FIXME(stephenfin): As above. + // Host: apiURL.Hostname(), + // Port: int32(port), + Host: platformStatus.APIServerInternalIPs[0], + Port: 6443, + }, + DisableAPIServerFloatingIP: ptr.To(true), + IdentityRef: openstackv1.OpenStackIdentityReference{ + Name: "openstack-cloud-credentials", + CloudName: "openstack", + }, + // NOTE(stephenfin): We deliberately don't add subnet here: CAPO will use all subnets in network, + // which should also cover dual stack deployments. Everything else is populated below. + Network: nil, + Router: nil, + ExternalNetwork: nil, + Subnets: nil, + Tags: []string{"openshiftClusterID=" + r.Infra.Status.InfrastructureName}, + }, + } + + // FIXME(stephenfin): Where can I source caCertificates from? The legacy infracluster controller + // had the same issue. + caCertificates := []byte{} // PEM encoded CA certificates + scopeFactory := openstackscope.NewFactory(0) + + scope, err := scopeFactory.NewClientScopeFromObject(ctx, r.Client, caCertificates, log, target) + if err != nil { + return nil, fmt.Errorf("creating OpenStack client: %w", err) + } + + networkClient, err := scope.NewNetworkClient() + if err != nil { + return nil, fmt.Errorf("creating OpenStack Networking client: %w", err) + } + + defaultSubnet, err := getDefaultSubnetFromMachines(ctx, log, r.Client, networkClient, platformStatus) + if err != nil { + return nil, err + } + // NOTE(stephenfin): As noted previously, we deliberately *do not* add subnet here: CAPO will + // use all subnets in network, which should also cover dual-stack deployments + target.Spec.Network = &openstackv1.NetworkParam{ID: ptr.To(defaultSubnet.NetworkID)} + + router, err := getDefaultRouterFromSubnet(ctx, networkClient, defaultSubnet) + if err != nil { + return nil, err + } + + target.Spec.Router = &openstackv1.RouterParam{ID: ptr.To(router.ID)} + // NOTE(stephenfin): The only reason we set ExternalNetworkID in the cluster spec is to avoid + // an error reconciling the external network if it isn't set. If CAPO ever no longer requires + // this we can just not set it and remove much of the code above. We don't actually use it. + target.Spec.ExternalNetwork = &openstackv1.NetworkParam{ID: ptr.To(router.GatewayInfo.NetworkID)} + + if err := r.Create(ctx, target); err != nil { + return nil, fmt.Errorf("failed to create InfraCluster: %w", err) + } + + log.Info(fmt.Sprintf("InfraCluster %s successfully created", klog.KObj(target))) + + return target, nil +} + +// getDefaultRouterFromSubnet attempts to infer the default router used for +// the network by looking for ports with the given subnet and gateway IP +// associated with them. +func getDefaultRouterFromSubnet(_ context.Context, networkClient openstackclients.NetworkClient, subnet *subnets.Subnet) (*routers.Router, error) { + // Find the port which owns the subnet's gateway IP + ports, err := networkClient.ListPort(ports.ListOpts{ + NetworkID: subnet.NetworkID, + FixedIPs: []ports.FixedIPOpts{ + { + IPAddress: subnet.GatewayIP, + // XXX: We should search on both subnet and IP + // address here, but can't because of + // https://github.com/gophercloud/gophercloud/issues/2807 + // SubnetID: subnet.ID, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("listing ports: %w", err) + } + + if len(ports) == 0 { + return nil, fmt.Errorf("%w: no ports found for subnet %s", errOpenStackNoDefaultRouter, subnet.ID) + } + + if len(ports) > 1 { + return nil, fmt.Errorf("%w: multiple ports found for subnet %s", errOpenStackNoDefaultRouter, subnet.ID) + } + + routerID := ports[0].DeviceID + + router, err := networkClient.GetRouter(routerID) + if err != nil { + return nil, fmt.Errorf("getting router %s: %w", routerID, err) + } + + if router.GatewayInfo.NetworkID == "" { + return nil, fmt.Errorf("%w: router %s does not have an external gateway", errOpenStackNoDefaultRouter, routerID) + } + + return router, nil +} + +// getDefaultSubnetFromMachines attempts to infer the default cluster subnet by +// directly examining the control plane machines. Specifically it looks for a +// subnet attached to a control plane machine whose CIDR contains the API +// loadbalancer internal VIP. +// +// This heuristic is only valid when the API loadbalancer type is +// LoadBalancerTypeOpenShiftManagedDefault. +// +//nolint:gocognit,funlen +func getDefaultSubnetFromMachines(ctx context.Context, log logr.Logger, kubeclient client.Client, networkClient openstackclients.NetworkClient, platformStatus *configv1.OpenStackPlatformStatus) (*subnets.Subnet, error) { + mapiMachines := mapiv1beta1.MachineList{} + if err := kubeclient.List( + ctx, + &mapiMachines, + client.InNamespace(defaultMAPINamespace), + client.MatchingLabels{"machine.openshift.io/cluster-api-machine-role": "master"}, + ); err != nil { + return nil, fmt.Errorf("listing control plane machines: %w", err) + } + + if len(mapiMachines.Items) == 0 { + return nil, errOpenStackNoControlPlaneMachines + } + + apiServerInternalIPs := make([]net.IP, len(platformStatus.APIServerInternalIPs)) + for i, ipStr := range platformStatus.APIServerInternalIPs { + apiServerInternalIPs[i] = net.ParseIP(ipStr) + } + + for _, mapiMachine := range mapiMachines.Items { + log := log.WithValues("machine", mapiMachine.Name) + + providerID := mapiMachine.Spec.ProviderID + if providerID == nil { + log.V(3).Info("Skipping machine: providerID is not set") + continue + } + + if !strings.HasPrefix(*providerID, "openstack:///") { + log.V(2).Info("Skipping machine: providerID has unexpected format", "providerID", *providerID) + continue + } + + instanceID := (*providerID)[len("openstack:///"):] + + portOpts := ports.ListOpts{ + DeviceID: instanceID, + } + + ports, err := networkClient.ListPort(portOpts) + if err != nil { + return nil, fmt.Errorf("listing ports for instance %s: %w", instanceID, err) + } + + if len(ports) == 0 { + return nil, fmt.Errorf("%w: no ports found for instance %s", errOpenStackNoDefaultSubnet, instanceID) + } + + for _, port := range ports { + log := log.WithValues("port", port.ID) + + for _, fixedIP := range port.FixedIPs { + if fixedIP.SubnetID == "" { + continue + } + + subnet, err := networkClient.GetSubnet(fixedIP.SubnetID) + if err != nil { + return nil, fmt.Errorf("getting subnet %s: %w", fixedIP.SubnetID, err) + } + + _, cidr, err := net.ParseCIDR(subnet.CIDR) + if err != nil { + return nil, fmt.Errorf("parsing subnet CIDR %s: %w", subnet.CIDR, err) + } + + if slices.ContainsFunc( + apiServerInternalIPs, func(ip net.IP) bool { return cidr.Contains(ip) }, + ) { + return subnet, nil + } + + log.V(6).Info("subnet does not match any APIServerInternalIPs", "subnet", subnet.CIDR) + } + + log.V(6).Info("port does not match any APIServerInternalIPs") + } + + log.V(6).Info("machine does not match any APIServerInternalIPs") + } + + return nil, fmt.Errorf("%w: no matching subnets found", errOpenStackNoDefaultSubnet) +} diff --git a/pkg/controllers/infracluster/powervs.go b/pkg/controllers/infracluster/powervs.go index ed26ec77f..412db9a87 100644 --- a/pkg/controllers/infracluster/powervs.go +++ b/pkg/controllers/infracluster/powervs.go @@ -36,11 +36,12 @@ import ( ) var ( + errInvalidPlatformStatus = errors.New("infrastructure PlatformStatus should not be nil") errUnKnownServiceInstanceType = errors.New("unknown service instance type") errUnKnownNetworkType = errors.New("unknown network type") ) -// ensureIBMPowerVSCluster ensures the IBMPowerVS cluster object exists. +// ensureIBMPowerVSCluster ensures the IBMPowerVSCluster object exists. // //nolint:funlen func (r *InfraClusterController) ensureIBMPowerVSCluster(ctx context.Context, log logr.Logger) (client.Object, error) { @@ -71,7 +72,7 @@ func (r *InfraClusterController) ensureIBMPowerVSCluster(ctx context.Context, lo } if r.Infra.Status.PlatformStatus == nil { - return nil, fmt.Errorf("infrastructure PlatformStatus should not be nil: %w", err) + return nil, errInvalidPlatformStatus } // Derive service instance and network from machine spec diff --git a/vendor/github.com/gofrs/uuid/v5/.gitignore b/vendor/github.com/gofrs/uuid/v5/.gitignore new file mode 100644 index 000000000..666dbbb5b --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# binary bundle generated by go-fuzz +uuid-fuzz.zip diff --git a/vendor/github.com/gofrs/uuid/v5/.pre-commit-config.yaml b/vendor/github.com/gofrs/uuid/v5/.pre-commit-config.yaml new file mode 100644 index 000000000..919f2c44b --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: +- repo: https://github.com/gitleaks/gitleaks + rev: v8.16.3 + hooks: + - id: gitleaks +- repo: https://github.com/golangci/golangci-lint + rev: v1.52.2 + hooks: + - id: golangci-lint +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/vendor/github.com/gofrs/uuid/v5/LICENSE b/vendor/github.com/gofrs/uuid/v5/LICENSE new file mode 100644 index 000000000..926d54987 --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/LICENSE @@ -0,0 +1,20 @@ +Copyright (C) 2013-2018 by Maxim Bublis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/gofrs/uuid/v5/README.md b/vendor/github.com/gofrs/uuid/v5/README.md new file mode 100644 index 000000000..f26206e0d --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/README.md @@ -0,0 +1,92 @@ +# UUID + +[![License](https://img.shields.io/github/license/gofrs/uuid.svg)](https://github.com/gofrs/uuid/blob/master/LICENSE) +[![Build Status](https://github.com/gofrs/uuid/actions/workflows/go.yml/badge.svg)](https://github.com/gofrs/uuid/actions/workflows/go.yml) +[![Go Reference](https://pkg.go.dev/badge/github.com/gofrs/uuid/v5.svg)](https://pkg.go.dev/github.com/gofrs/uuid/v5) +[![Coverage Status](https://codecov.io/gh/gofrs/uuid/branch/master/graphs/badge.svg?branch=master)](https://codecov.io/gh/gofrs/uuid/) +[![Go Report Card](https://goreportcard.com/badge/github.com/gofrs/uuid)](https://goreportcard.com/report/github.com/gofrs/uuid) +[![CodeQL](https://github.com/gofrs/uuid/actions/workflows/codeql.yml/badge.svg)](https://github.com/gofrs/uuid/actions/workflows/codeql.yml) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8929/badge)](https://www.bestpractices.dev/projects/8929) +[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/gofrs/uuid/badge)](https://scorecard.dev/viewer/?uri=github.com/gofrs/uuid) + +Package uuid provides a pure Go implementation of Universally Unique Identifiers +(UUID) variant as defined in RFC-9562. This package supports both the creation +and parsing of UUIDs in different formats. + +This package supports the following UUID versions: + +* Version 1, based on timestamp and MAC address +* Version 3, based on MD5 hashing of a named value +* Version 4, based on random numbers +* Version 5, based on SHA-1 hashing of a named value +* Version 6, a k-sortable id based on timestamp, and field-compatible with v1 +* Version 7, a k-sortable id based on timestamp + +## Project History + +This project was originally forked from the +[github.com/satori/go.uuid](https://github.com/satori/go.uuid) repository after +it appeared to be no longer maintained, while exhibiting [critical +flaws](https://github.com/satori/go.uuid/issues/73). We have decided to take +over this project to ensure it receives regular maintenance for the benefit of +the larger Go community. + +We'd like to thank Maxim Bublis for his hard work on the original iteration of +the package. + +## License + +This source code of this package is released under the MIT License. Please see +the [LICENSE](https://github.com/gofrs/uuid/blob/master/LICENSE) for the full +content of the license. + +## Recommended Package Version + +We recommend using v2.0.0+ of this package, as versions prior to 2.0.0 were +created before our fork of the original package and have some known +deficiencies. + +## Requirements + +This package requires Go 1.19 or later + +## Usage + +Here is a quick overview of how to use this package. For more detailed +documentation, please see the [GoDoc Page](http://godoc.org/github.com/gofrs/uuid). + +```go +package main + +import ( + "log" + + "github.com/gofrs/uuid/v5" +) + +// Create a Version 4 UUID, panicking on error. +// Use this form to initialize package-level variables. +var u1 = uuid.Must(uuid.NewV4()) + +func main() { + // Create a Version 4 UUID. + u2, err := uuid.NewV4() + if err != nil { + log.Fatalf("failed to generate UUID: %v", err) + } + log.Printf("generated Version 4 UUID %v", u2) + + // Parse a UUID from a string. + s := "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + u3, err := uuid.FromString(s) + if err != nil { + log.Fatalf("failed to parse UUID %q: %v", s, err) + } + log.Printf("successfully parsed UUID %v", u3) +} +``` + +## References + +* [RFC-9562](https://tools.ietf.org/html/rfc9562) (replaces RFC-4122) +* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01) diff --git a/vendor/github.com/gofrs/uuid/v5/SECURITY.md b/vendor/github.com/gofrs/uuid/v5/SECURITY.md new file mode 100644 index 000000000..e09411e8c --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/SECURITY.md @@ -0,0 +1,22 @@ +# Security Policy + +## Supported Versions + +We support the latest version of this library. We do not guarantee support of previous versions. If a defect is reported, it will generally be fixed on the latest version +(provided it exists) irrespective of whether it was introduced in a prior version. + +## Reporting a Vulnerability + +If you discover a vulnerability against this package, please report it in the issues tab with a `vulnerability` label. We will examine promptly. + +If you would like to disclose the vulnerability privately, you may reach the maintainers in our [channel](https://gophers.slack.com/archives/CBP4N9BEU) on the gophers slack. + +## Security Scorecard + +This project submits security [results](https://scorecard.dev/viewer/?uri=github.com/gofrs/uuid) to the [OpenSSF Scorecard](https://securityscorecards.dev/). + +### Actively Maintained + +One heuristic these scorecards measure to gauge whether a package is safe for consumption is an "Actively Maintained" metric. Because this library implements UUIDs, +it is very stable - there is not much maintenance required other than adding/updating newer UUID versions, keeping up to date with latest versions of Go, and responding +to reported exploits. As a result, periods of low active maintenance are to be expected. diff --git a/vendor/github.com/gofrs/uuid/v5/codec.go b/vendor/github.com/gofrs/uuid/v5/codec.go new file mode 100644 index 000000000..8087955de --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/codec.go @@ -0,0 +1,229 @@ +// Copyright (C) 2013-2018 by Maxim Bublis +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package uuid + +import "fmt" + +// FromBytes returns a UUID generated from the raw byte slice input. +// It will return an error if the slice isn't 16 bytes long. +func FromBytes(input []byte) (UUID, error) { + u := UUID{} + err := u.UnmarshalBinary(input) + return u, err +} + +// FromBytesOrNil returns a UUID generated from the raw byte slice input. +// Same behavior as FromBytes(), but returns uuid.Nil instead of an error. +func FromBytesOrNil(input []byte) UUID { + uuid, err := FromBytes(input) + if err != nil { + return Nil + } + return uuid +} + +func fromHexChar(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 255 +} + +// Parse parses the UUID stored in the string text. Parsing and supported +// formats are the same as UnmarshalText. +func (u *UUID) Parse(s string) error { + switch len(s) { + case 32: // hash + case 36: // canonical + case 34, 38: + if s[0] != '{' || s[len(s)-1] != '}' { + return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s) + } + s = s[1 : len(s)-1] + case 41, 45: + if s[:9] != "urn:uuid:" { + return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s[:9]) + } + s = s[9:] + default: + return fmt.Errorf("%w %d in string %q", ErrIncorrectLength, len(s), s) + } + // canonical + if len(s) == 36 { + if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { + return fmt.Errorf("%w %q", ErrIncorrectFormatInString, s) + } + for i, x := range [16]byte{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v1 := fromHexChar(s[x]) + v2 := fromHexChar(s[x+1]) + if v1|v2 == 255 { + return ErrInvalidFormat + } + u[i] = (v1 << 4) | v2 + } + return nil + } + // hash like + for i := 0; i < 32; i += 2 { + v1 := fromHexChar(s[i]) + v2 := fromHexChar(s[i+1]) + if v1|v2 == 255 { + return ErrInvalidFormat + } + u[i/2] = (v1 << 4) | v2 + } + return nil +} + +// FromString returns a UUID parsed from the input string. +// Input is expected in a form accepted by UnmarshalText. +func FromString(text string) (UUID, error) { + var u UUID + err := u.Parse(text) + return u, err +} + +// FromStringOrNil returns a UUID parsed from the input string. +// Same behavior as FromString(), but returns uuid.Nil instead of an error. +func FromStringOrNil(input string) UUID { + uuid, err := FromString(input) + if err != nil { + return Nil + } + return uuid +} + +// MarshalText implements the encoding.TextMarshaler interface. +// The encoding is the same as returned by the String() method. +func (u UUID) MarshalText() ([]byte, error) { + var buf [36]byte + encodeCanonical(buf[:], u) + return buf[:], nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// Following formats are supported: +// +// "6ba7b810-9dad-11d1-80b4-00c04fd430c8", +// "{6ba7b810-9dad-11d1-80b4-00c04fd430c8}", +// "urn:uuid:6ba7b810-9dad-11d1-80b4-00c04fd430c8" +// "6ba7b8109dad11d180b400c04fd430c8" +// "{6ba7b8109dad11d180b400c04fd430c8}", +// "urn:uuid:6ba7b8109dad11d180b400c04fd430c8" +// +// ABNF for supported UUID text representation follows: +// +// URN := 'urn' +// UUID-NID := 'uuid' +// +// hexdig := '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | +// 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | +// 'A' | 'B' | 'C' | 'D' | 'E' | 'F' +// +// hexoct := hexdig hexdig +// 2hexoct := hexoct hexoct +// 4hexoct := 2hexoct 2hexoct +// 6hexoct := 4hexoct 2hexoct +// 12hexoct := 6hexoct 6hexoct +// +// hashlike := 12hexoct +// canonical := 4hexoct '-' 2hexoct '-' 2hexoct '-' 6hexoct +// +// plain := canonical | hashlike +// uuid := canonical | hashlike | braced | urn +// +// braced := '{' plain '}' | '{' hashlike '}' +// urn := URN ':' UUID-NID ':' plain +func (u *UUID) UnmarshalText(b []byte) error { + switch len(b) { + case 32: // hash + case 36: // canonical + case 34, 38: + if b[0] != '{' || b[len(b)-1] != '}' { + return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b) + } + b = b[1 : len(b)-1] + case 41, 45: + if string(b[:9]) != "urn:uuid:" { + return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b[:9]) + } + b = b[9:] + default: + return fmt.Errorf("%w %d in string %q", ErrIncorrectLength, len(b), b) + } + if len(b) == 36 { + if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { + return fmt.Errorf("%w %q", ErrIncorrectFormatInString, b) + } + for i, x := range [16]byte{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + v1 := fromHexChar(b[x]) + v2 := fromHexChar(b[x+1]) + if v1|v2 == 255 { + return ErrInvalidFormat + } + u[i] = (v1 << 4) | v2 + } + return nil + } + for i := 0; i < 32; i += 2 { + v1 := fromHexChar(b[i]) + v2 := fromHexChar(b[i+1]) + if v1|v2 == 255 { + return ErrInvalidFormat + } + u[i/2] = (v1 << 4) | v2 + } + return nil +} + +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (u UUID) MarshalBinary() ([]byte, error) { + return u.Bytes(), nil +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +// It will return an error if the slice isn't 16 bytes long. +func (u *UUID) UnmarshalBinary(data []byte) error { + if len(data) != Size { + return fmt.Errorf("%w, got %d bytes", ErrIncorrectByteLength, len(data)) + } + copy(u[:], data) + + return nil +} diff --git a/vendor/github.com/gofrs/uuid/v5/error.go b/vendor/github.com/gofrs/uuid/v5/error.go new file mode 100644 index 000000000..7ca0be498 --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/error.go @@ -0,0 +1,40 @@ +package uuid + +// Error is a custom error type for UUID-related errors +type Error string + +// The strings defined in the errors is matching the previous behavior before +// the custom error type was implemented. The reason is that some people might +// be relying on the exact string representation to handle errors in their code. +const ( + // ErrInvalidFormat is returned when the UUID string representation does not + // match the expected format. See also ErrIncorrectFormatInString. + ErrInvalidFormat = Error("uuid: invalid UUID format") + + // ErrIncorrectFormatInString can be returned instead of ErrInvalidFormat. + // A separate error type is used because of how errors used to be formatted + // before custom error types were introduced. + ErrIncorrectFormatInString = Error("uuid: incorrect UUID format in string") + + // ErrIncorrectLength is returned when the UUID does not have the + // appropriate string length for parsing the UUID. + ErrIncorrectLength = Error("uuid: incorrect UUID length") + + // ErrIncorrectByteLength indicates the UUID byte slice length is invalid. + ErrIncorrectByteLength = Error("uuid: UUID must be exactly 16 bytes long") + + // ErrNoHwAddressFound is returned when a hardware (MAC) address cannot be + // found for UUID generation. + ErrNoHwAddressFound = Error("uuid: no HW address found") + + // ErrTypeConvertError is returned for type conversion operation fails. + ErrTypeConvertError = Error("uuid: cannot convert") + + // ErrInvalidVersion indicates an unsupported or invalid UUID version. + ErrInvalidVersion = Error("uuid:") +) + +// Error returns the string representation of the UUID error. +func (e Error) Error() string { + return string(e) +} diff --git a/vendor/github.com/gofrs/uuid/v5/generator.go b/vendor/github.com/gofrs/uuid/v5/generator.go new file mode 100644 index 000000000..02cba76ae --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/generator.go @@ -0,0 +1,491 @@ +// Copyright (C) 2013-2018 by Maxim Bublis +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package uuid + +import ( + "crypto/md5" + "crypto/rand" + "crypto/sha1" + "encoding/binary" + "hash" + "io" + "net" + "sync" + "time" +) + +// Difference in 100-nanosecond intervals between +// UUID epoch (October 15, 1582) and Unix epoch (January 1, 1970). +const epochStart = 122192928000000000 + +// EpochFunc is the function type used to provide the current time. +type EpochFunc func() time.Time + +// HWAddrFunc is the function type used to provide hardware (MAC) addresses. +type HWAddrFunc func() (net.HardwareAddr, error) + +// DefaultGenerator is the default UUID Generator used by this package. +var DefaultGenerator Generator = NewGen() + +// NewV1 returns a UUID based on the current timestamp and MAC address. +func NewV1() (UUID, error) { + return DefaultGenerator.NewV1() +} + +// NewV1 returns a UUID based on the provided timestamp and MAC address. +func NewV1AtTime(atTime time.Time) (UUID, error) { + return DefaultGenerator.NewV1AtTime(atTime) +} + +// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name. +func NewV3(ns UUID, name string) UUID { + return DefaultGenerator.NewV3(ns, name) +} + +// NewV4 returns a randomly generated UUID. +func NewV4() (UUID, error) { + return DefaultGenerator.NewV4() +} + +// NewV5 returns a UUID based on SHA-1 hash of the namespace UUID and name. +func NewV5(ns UUID, name string) UUID { + return DefaultGenerator.NewV5(ns, name) +} + +// NewV6 returns a k-sortable UUID based on the current timestamp and 48 bits of +// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit +// order being adjusted to allow the UUID to be k-sortable. +func NewV6() (UUID, error) { + return DefaultGenerator.NewV6() +} + +// NewV6 returns a k-sortable UUID based on the provided timestamp and 48 bits of +// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit +// order being adjusted to allow the UUID to be k-sortable. +func NewV6AtTime(atTime time.Time) (UUID, error) { + return DefaultGenerator.NewV6AtTime(atTime) +} + +// NewV7 returns a k-sortable UUID based on the current millisecond-precision +// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch +// generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter. +func NewV7() (UUID, error) { + return DefaultGenerator.NewV7() +} + +// NewV7 returns a k-sortable UUID based on the provided millisecond-precision +// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch +// generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter. +func NewV7AtTime(atTime time.Time) (UUID, error) { + return DefaultGenerator.NewV7AtTime(atTime) +} + +// Generator provides an interface for generating UUIDs. +type Generator interface { + NewV1() (UUID, error) + NewV1AtTime(time.Time) (UUID, error) + NewV3(ns UUID, name string) UUID + NewV4() (UUID, error) + NewV5(ns UUID, name string) UUID + NewV6() (UUID, error) + NewV6AtTime(time.Time) (UUID, error) + NewV7() (UUID, error) + NewV7AtTime(time.Time) (UUID, error) +} + +// Gen is a reference UUID generator based on the specifications laid out in +// RFC-9562 and DCE 1.1: Authentication and Security Services. This type +// satisfies the Generator interface as defined in this package. +// +// For consumers who are generating V1 UUIDs, but don't want to expose the MAC +// address of the node generating the UUIDs, the NewGenWithHWAF() function has been +// provided as a convenience. See the function's documentation for more info. +// +// The authors of this package do not feel that the majority of users will need +// to obfuscate their MAC address, and so we recommend using NewGen() to create +// a new generator. +type Gen struct { + clockSequenceOnce sync.Once + hardwareAddrOnce sync.Once + storageMutex sync.Mutex + + rand io.Reader + + epochFunc EpochFunc + hwAddrFunc HWAddrFunc + lastTime uint64 + clockSequence uint16 + hardwareAddr [6]byte +} + +// GenOption is a function type that can be used to configure a Gen generator. +type GenOption func(*Gen) + +// interface check -- build will fail if *Gen doesn't satisfy Generator +var _ Generator = (*Gen)(nil) + +// NewGen returns a new instance of Gen with some default values set. Most +// people should use this. +func NewGen() *Gen { + return NewGenWithHWAF(defaultHWAddrFunc) +} + +// NewGenWithHWAF builds a new UUID generator with the HWAddrFunc provided. Most +// consumers should use NewGen() instead. +// +// This is used so that consumers can generate their own MAC addresses, for use +// in the generated UUIDs, if there is some concern about exposing the physical +// address of the machine generating the UUID. +// +// The Gen generator will only invoke the HWAddrFunc once, and cache that MAC +// address for all the future UUIDs generated by it. If you'd like to switch the +// MAC address being used, you'll need to create a new generator using this +// function. +func NewGenWithHWAF(hwaf HWAddrFunc) *Gen { + return NewGenWithOptions(WithHWAddrFunc(hwaf)) +} + +// NewGenWithOptions returns a new instance of Gen with the options provided. +// Most people should use NewGen() or NewGenWithHWAF() instead. +// +// To customize the generator, you can pass in one or more GenOption functions. +// For example: +// +// gen := NewGenWithOptions( +// WithHWAddrFunc(myHWAddrFunc), +// WithEpochFunc(myEpochFunc), +// WithRandomReader(myRandomReader), +// ) +// +// NewGenWithOptions(WithHWAddrFunc(myHWAddrFunc)) is equivalent to calling +// NewGenWithHWAF(myHWAddrFunc) +// NewGenWithOptions() is equivalent to calling NewGen() +func NewGenWithOptions(opts ...GenOption) *Gen { + gen := &Gen{ + epochFunc: time.Now, + hwAddrFunc: defaultHWAddrFunc, + rand: rand.Reader, + } + + for _, opt := range opts { + opt(gen) + } + + return gen +} + +// WithHWAddrFunc is a GenOption that allows you to provide your own HWAddrFunc +// function. +// When this option is nil, the defaultHWAddrFunc is used. +func WithHWAddrFunc(hwaf HWAddrFunc) GenOption { + return func(gen *Gen) { + if hwaf == nil { + hwaf = defaultHWAddrFunc + } + + gen.hwAddrFunc = hwaf + } +} + +// WithEpochFunc is a GenOption that allows you to provide your own EpochFunc +// function. +// When this option is nil, time.Now is used. +func WithEpochFunc(epochf EpochFunc) GenOption { + return func(gen *Gen) { + if epochf == nil { + epochf = time.Now + } + + gen.epochFunc = epochf + } +} + +// WithRandomReader is a GenOption that allows you to provide your own random +// reader. +// When this option is nil, the default rand.Reader is used. +func WithRandomReader(reader io.Reader) GenOption { + return func(gen *Gen) { + if reader == nil { + reader = rand.Reader + } + + gen.rand = reader + } +} + +// NewV1 returns a UUID based on the current timestamp and MAC address. +func (g *Gen) NewV1() (UUID, error) { + return g.NewV1AtTime(g.epochFunc()) +} + +// NewV1AtTime returns a UUID based on the provided timestamp and current MAC address. +func (g *Gen) NewV1AtTime(atTime time.Time) (UUID, error) { + u := UUID{} + + timeNow, clockSeq, err := g.getClockSequence(false, atTime) + if err != nil { + return Nil, err + } + binary.BigEndian.PutUint32(u[0:], uint32(timeNow)) + binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>32)) + binary.BigEndian.PutUint16(u[6:], uint16(timeNow>>48)) + binary.BigEndian.PutUint16(u[8:], clockSeq) + + hardwareAddr, err := g.getHardwareAddr() + if err != nil { + return Nil, err + } + copy(u[10:], hardwareAddr) + + u.SetVersion(V1) + u.SetVariant(VariantRFC9562) + + return u, nil +} + +// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name. +func (g *Gen) NewV3(ns UUID, name string) UUID { + u := newFromHash(md5.New(), ns, name) + u.SetVersion(V3) + u.SetVariant(VariantRFC9562) + + return u +} + +// NewV4 returns a randomly generated UUID. +func (g *Gen) NewV4() (UUID, error) { + u := UUID{} + if _, err := io.ReadFull(g.rand, u[:]); err != nil { + return Nil, err + } + u.SetVersion(V4) + u.SetVariant(VariantRFC9562) + + return u, nil +} + +// NewV5 returns a UUID based on SHA-1 hash of the namespace UUID and name. +func (g *Gen) NewV5(ns UUID, name string) UUID { + u := newFromHash(sha1.New(), ns, name) + u.SetVersion(V5) + u.SetVariant(VariantRFC9562) + + return u +} + +// NewV6 returns a k-sortable UUID based on the current timestamp and 48 bits of +// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit +// order being adjusted to allow the UUID to be k-sortable. +func (g *Gen) NewV6() (UUID, error) { + return g.NewV6AtTime(g.epochFunc()) +} + +// NewV6 returns a k-sortable UUID based on the provided timestamp and 48 bits of +// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit +// order being adjusted to allow the UUID to be k-sortable. +func (g *Gen) NewV6AtTime(atTime time.Time) (UUID, error) { + /* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-6 + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | time_high | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | time_mid | ver | time_low | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |var| clock_seq | node | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | node | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ + var u UUID + + timeNow, _, err := g.getClockSequence(false, atTime) + if err != nil { + return Nil, err + } + + binary.BigEndian.PutUint32(u[0:], uint32(timeNow>>28)) // set time_high + binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>12)) // set time_mid + binary.BigEndian.PutUint16(u[6:], uint16(timeNow&0xfff)) // set time_low (minus four version bits) + + // Based on the RFC 9562 recommendation that this data be fully random and not a monotonic counter, + //we do NOT support batching version 6 UUIDs. + //set clock_seq (14 bits) and node (48 bits) pseudo-random bits (first 2 bits will be overridden) + if _, err = io.ReadFull(g.rand, u[8:]); err != nil { + return Nil, err + } + + u.SetVersion(V6) + + //overwrite first 2 bits of byte[8] for the variant + u.SetVariant(VariantRFC9562) + + return u, nil +} + +// NewV7 returns a k-sortable UUID based on the current millisecond-precision +// UNIX epoch and 74 bits of pseudorandom data. +func (g *Gen) NewV7() (UUID, error) { + return g.NewV7AtTime(g.epochFunc()) +} + +// NewV7 returns a k-sortable UUID based on the provided millisecond-precision +// UNIX epoch and 74 bits of pseudorandom data. +func (g *Gen) NewV7AtTime(atTime time.Time) (UUID, error) { + var u UUID + /* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7 + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | unix_ts_ms | ver | rand_a | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |var| rand_b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | rand_b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */ + + ms, clockSeq, err := g.getClockSequence(true, atTime) + if err != nil { + return Nil, err + } + //UUIDv7 features a 48 bit timestamp. First 32bit (4bytes) represents seconds since 1970, followed by 2 bytes for the ms granularity. + u[0] = byte(ms >> 40) //1-6 bytes: big-endian unsigned number of Unix epoch timestamp + u[1] = byte(ms >> 32) + u[2] = byte(ms >> 24) + u[3] = byte(ms >> 16) + u[4] = byte(ms >> 8) + u[5] = byte(ms) + + //Support batching by using a monotonic pseudo-random sequence, + //as described in RFC 9562 section 6.2, Method 1. + //The 6th byte contains the version and partially rand_a data. + //We will lose the most significant bites from the clockSeq (with SetVersion), but it is ok, + //we need the least significant that contains the counter to ensure the monotonic property + binary.BigEndian.PutUint16(u[6:8], clockSeq) // set rand_a with clock seq which is random and monotonic + + //override first 4bits of u[6]. + u.SetVersion(V7) + + //set rand_b 64bits of pseudo-random bits (first 2 will be overridden) + if _, err = io.ReadFull(g.rand, u[8:16]); err != nil { + return Nil, err + } + //override first 2 bits of byte[8] for the variant + u.SetVariant(VariantRFC9562) + + return u, nil +} + +// getClockSequence returns the epoch and clock sequence of the provided time, +// used for generating V1,V6 and V7 UUIDs. +// +// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of +// 100-nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian +// reform to the Christian calendar). +func (g *Gen) getClockSequence(useUnixTSMs bool, atTime time.Time) (uint64, uint16, error) { + var err error + g.clockSequenceOnce.Do(func() { + buf := make([]byte, 2) + if _, err = io.ReadFull(g.rand, buf); err != nil { + return + } + g.clockSequence = binary.BigEndian.Uint16(buf) + }) + if err != nil { + return 0, 0, err + } + + g.storageMutex.Lock() + defer g.storageMutex.Unlock() + + var timeNow uint64 + if useUnixTSMs { + timeNow = uint64(atTime.UnixMilli()) + } else { + timeNow = g.getEpoch(atTime) + } + // Clock didn't change since last UUID generation. + // Should increase clock sequence. + if timeNow <= g.lastTime { + g.clockSequence++ + } + g.lastTime = timeNow + + return timeNow, g.clockSequence, nil +} + +// Returns the hardware address. +func (g *Gen) getHardwareAddr() ([]byte, error) { + var err error + g.hardwareAddrOnce.Do(func() { + var hwAddr net.HardwareAddr + if hwAddr, err = g.hwAddrFunc(); err == nil { + copy(g.hardwareAddr[:], hwAddr) + return + } + + // Initialize hardwareAddr randomly in case + // of real network interfaces absence. + if _, err = io.ReadFull(g.rand, g.hardwareAddr[:]); err != nil { + return + } + // Set multicast bit as recommended by RFC-9562 + g.hardwareAddr[0] |= 0x01 + }) + if err != nil { + return []byte{}, err + } + return g.hardwareAddr[:], nil +} + +// Returns the difference between UUID epoch (October 15, 1582) +// and the provided time in 100-nanosecond intervals. +func (g *Gen) getEpoch(atTime time.Time) uint64 { + return epochStart + uint64(atTime.UnixNano()/100) +} + +// Returns the UUID based on the hashing of the namespace UUID and name. +func newFromHash(h hash.Hash, ns UUID, name string) UUID { + u := UUID{} + h.Write(ns[:]) + h.Write([]byte(name)) + copy(u[:], h.Sum(nil)) + + return u +} + +var netInterfaces = net.Interfaces + +// Returns the hardware address. +func defaultHWAddrFunc() (net.HardwareAddr, error) { + ifaces, err := netInterfaces() + if err != nil { + return []byte{}, err + } + for _, iface := range ifaces { + if len(iface.HardwareAddr) >= 6 { + return iface.HardwareAddr, nil + } + } + return []byte{}, ErrNoHwAddressFound +} diff --git a/vendor/github.com/gofrs/uuid/v5/sql.go b/vendor/github.com/gofrs/uuid/v5/sql.go new file mode 100644 index 000000000..cdc751820 --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/sql.go @@ -0,0 +1,116 @@ +// Copyright (C) 2013-2018 by Maxim Bublis +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +package uuid + +import ( + "database/sql" + "database/sql/driver" + "fmt" +) + +var _ driver.Valuer = UUID{} +var _ sql.Scanner = (*UUID)(nil) + +// Value implements the driver.Valuer interface. +func (u UUID) Value() (driver.Value, error) { + return u.String(), nil +} + +// Scan implements the sql.Scanner interface. +// A 16-byte slice will be handled by UnmarshalBinary, while +// a longer byte slice or a string will be handled by UnmarshalText. +func (u *UUID) Scan(src interface{}) error { + switch src := src.(type) { + case UUID: // support gorm convert from UUID to NullUUID + *u = src + return nil + + case []byte: + if len(src) == Size { + return u.UnmarshalBinary(src) + } + return u.UnmarshalText(src) + + case string: + uu, err := FromString(src) + *u = uu + return err + } + + return fmt.Errorf("%w %T to UUID", ErrTypeConvertError, src) +} + +// NullUUID can be used with the standard sql package to represent a +// UUID value that can be NULL in the database. +type NullUUID struct { + UUID UUID + Valid bool +} + +// Value implements the driver.Valuer interface. +func (u NullUUID) Value() (driver.Value, error) { + if !u.Valid { + return nil, nil + } + // Delegate to UUID Value function + return u.UUID.Value() +} + +// Scan implements the sql.Scanner interface. +func (u *NullUUID) Scan(src interface{}) error { + if src == nil { + u.UUID, u.Valid = Nil, false + return nil + } + + // Delegate to UUID Scan function + u.Valid = true + return u.UUID.Scan(src) +} + +var nullJSON = []byte("null") + +// MarshalJSON marshals the NullUUID as null or the nested UUID +func (u NullUUID) MarshalJSON() ([]byte, error) { + if !u.Valid { + return nullJSON, nil + } + var buf [38]byte + buf[0] = '"' + encodeCanonical(buf[1:37], u.UUID) + buf[37] = '"' + return buf[:], nil +} + +// UnmarshalJSON unmarshals a NullUUID +func (u *NullUUID) UnmarshalJSON(b []byte) error { + if string(b) == "null" { + u.UUID, u.Valid = Nil, false + return nil + } + if n := len(b); n >= 2 && b[0] == '"' { + b = b[1 : n-1] + } + err := u.UUID.UnmarshalText(b) + u.Valid = (err == nil) + return err +} diff --git a/vendor/github.com/gofrs/uuid/v5/uuid.go b/vendor/github.com/gofrs/uuid/v5/uuid.go new file mode 100644 index 000000000..e6fc3fc10 --- /dev/null +++ b/vendor/github.com/gofrs/uuid/v5/uuid.go @@ -0,0 +1,320 @@ +// Copyright (C) 2013-2018 by Maxim Bublis +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +// Package uuid provides implementations of the Universally Unique Identifier +// (UUID), as specified in RFC-9562 (formerly RFC-4122). +// +// RFC-9562[1] provides the specification for versions 1, 3, 4, 5, 6 and 7. +// +// DCE 1.1[2] provides the specification for version 2, but version 2 support +// was removed from this package in v4 due to some concerns with the +// specification itself. Reading the spec, it seems that it would result in +// generating UUIDs that aren't very unique. In having read the spec it seemed +// that our implementation did not meet the spec. It also seems to be at-odds +// with RFC 9562, meaning we would need quite a bit of special code to support +// it. Lastly, there were no Version 2 implementations that we could find to +// ensure we were understanding the specification correctly. +// +// [1] https://tools.ietf.org/html/rfc9562 +// [2] http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01 +package uuid + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "time" +) + +// Size of a UUID in bytes. +const Size = 16 + +// UUID is an array type to represent the value of a UUID, as defined in RFC-9562. +type UUID [Size]byte + +// UUID versions. +const ( + _ byte = iota + V1 // Version 1 (date-time and MAC address) + _ // Version 2 (date-time and MAC address, DCE security version) [removed] + V3 // Version 3 (namespace name-based) + V4 // Version 4 (random) + V5 // Version 5 (namespace name-based) + V6 // Version 6 (k-sortable timestamp and random data, field-compatible with v1) + V7 // Version 7 (k-sortable timestamp and random data) + _ // Version 8 (k-sortable timestamp, meant for custom implementations) [not implemented] +) + +// UUID layout variants. +const ( + VariantNCS byte = iota + VariantRFC9562 + VariantMicrosoft + VariantFuture +) + +// Backward-compatible variant for RFC 4122 +const VariantRFC4122 = VariantRFC9562 + +// UUID DCE domains. +const ( + DomainPerson = iota + DomainGroup + DomainOrg +) + +// Timestamp is the count of 100-nanosecond intervals since 00:00:00.00, +// 15 October 1582 within a V1 UUID. This type has no meaning for other +// UUID versions since they don't have an embedded timestamp. +type Timestamp uint64 + +const _100nsPerSecond = 10000000 + +// Time returns the UTC time.Time representation of a Timestamp +func (t Timestamp) Time() (time.Time, error) { + secs := uint64(t) / _100nsPerSecond + nsecs := 100 * (uint64(t) % _100nsPerSecond) + + return time.Unix(int64(secs)-(epochStart/_100nsPerSecond), int64(nsecs)), nil +} + +// TimestampFromV1 returns the Timestamp embedded within a V1 UUID. +// Returns an error if the UUID is any version other than 1. +func TimestampFromV1(u UUID) (Timestamp, error) { + if u.Version() != 1 { + err := fmt.Errorf("%w %s is version %d, not version 1", ErrInvalidVersion, u, u.Version()) + return 0, err + } + + low := binary.BigEndian.Uint32(u[0:4]) + mid := binary.BigEndian.Uint16(u[4:6]) + hi := binary.BigEndian.Uint16(u[6:8]) & 0xfff + + return Timestamp(uint64(low) + (uint64(mid) << 32) + (uint64(hi) << 48)), nil +} + +// TimestampFromV6 returns the Timestamp embedded within a V6 UUID. This +// function returns an error if the UUID is any version other than 6. +func TimestampFromV6(u UUID) (Timestamp, error) { + if u.Version() != 6 { + return 0, fmt.Errorf("%w %s is version %d, not version 6", ErrInvalidVersion, u, u.Version()) + } + + hi := binary.BigEndian.Uint32(u[0:4]) + mid := binary.BigEndian.Uint16(u[4:6]) + low := binary.BigEndian.Uint16(u[6:8]) & 0xfff + + return Timestamp(uint64(low) + (uint64(mid) << 12) + (uint64(hi) << 28)), nil +} + +// TimestampFromV7 returns the Timestamp embedded within a V7 UUID. This +// function returns an error if the UUID is any version other than 7. +func TimestampFromV7(u UUID) (Timestamp, error) { + if u.Version() != 7 { + return 0, fmt.Errorf("%w %s is version %d, not version 7", ErrInvalidVersion, u, u.Version()) + } + + t := 0 | + (int64(u[0]) << 40) | + (int64(u[1]) << 32) | + (int64(u[2]) << 24) | + (int64(u[3]) << 16) | + (int64(u[4]) << 8) | + int64(u[5]) + + // convert to format expected by Timestamp + tsNanos := epochStart + time.UnixMilli(t).UTC().UnixNano()/100 + return Timestamp(tsNanos), nil +} + +// Nil is the nil UUID, as specified in RFC-9562, that has all 128 bits set to +// zero. +var Nil = UUID{} + +// Max is the maximum UUID, as specified in RFC-9562, that has all 128 bits +// set to one. +var Max = UUID{ + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, + 0xFF, +} + +// Predefined namespace UUIDs. +var ( + NamespaceDNS = Must(FromString("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) + NamespaceURL = Must(FromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) + NamespaceOID = Must(FromString("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) + NamespaceX500 = Must(FromString("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) +) + +// IsNil returns if the UUID is equal to the nil UUID +func (u UUID) IsNil() bool { + return u == Nil +} + +// Version returns the algorithm version used to generate the UUID. +func (u UUID) Version() byte { + return u[6] >> 4 +} + +// Variant returns the UUID layout variant. +func (u UUID) Variant() byte { + switch { + case (u[8] >> 7) == 0x00: + return VariantNCS + case (u[8] >> 6) == 0x02: + return VariantRFC9562 + case (u[8] >> 5) == 0x06: + return VariantMicrosoft + case (u[8] >> 5) == 0x07: + fallthrough + default: + return VariantFuture + } +} + +// Bytes returns a byte slice representation of the UUID. +func (u UUID) Bytes() []byte { + return u[:] +} + +// encodeCanonical encodes the canonical RFC-9562 form of UUID u into the +// first 36 bytes dst. +func encodeCanonical(dst []byte, u UUID) { + const hextable = "0123456789abcdef" + dst[8] = '-' + dst[13] = '-' + dst[18] = '-' + dst[23] = '-' + for i, x := range [16]byte{ + 0, 2, 4, 6, + 9, 11, + 14, 16, + 19, 21, + 24, 26, 28, 30, 32, 34, + } { + c := u[i] + dst[x] = hextable[c>>4] + dst[x+1] = hextable[c&0x0f] + } +} + +// String returns a canonical RFC-9562 string representation of the UUID: +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. +func (u UUID) String() string { + var buf [36]byte + encodeCanonical(buf[:], u) + return string(buf[:]) +} + +// Format implements fmt.Formatter for UUID values. +// +// The behavior is as follows: +// The 'x' and 'X' verbs output only the hex digits of the UUID, using a-f for 'x' and A-F for 'X'. +// The 'v', '+v', 's' and 'q' verbs return the canonical RFC-9562 string representation. +// The 'S' verb returns the RFC-9562 format, but with capital hex digits. +// The '#v' verb returns the "Go syntax" representation, which is a 16 byte array initializer. +// All other verbs not handled directly by the fmt package (like '%p') are unsupported and will return +// "%!verb(uuid.UUID=value)" as recommended by the fmt package. +func (u UUID) Format(f fmt.State, c rune) { + if c == 'v' && f.Flag('#') { + fmt.Fprintf(f, "%#v", [Size]byte(u)) + return + } + switch c { + case 'x', 'X': + b := make([]byte, 32) + hex.Encode(b, u[:]) + if c == 'X' { + toUpperHex(b) + } + _, _ = f.Write(b) + case 'v', 's', 'S': + b, _ := u.MarshalText() + if c == 'S' { + toUpperHex(b) + } + _, _ = f.Write(b) + case 'q': + b := make([]byte, 38) + b[0] = '"' + encodeCanonical(b[1:], u) + b[37] = '"' + _, _ = f.Write(b) + default: + // invalid/unsupported format verb + fmt.Fprintf(f, "%%!%c(uuid.UUID=%s)", c, u.String()) + } +} + +func toUpperHex(b []byte) { + for i, c := range b { + if 'a' <= c && c <= 'f' { + b[i] = c - ('a' - 'A') + } + } +} + +// SetVersion sets the version bits. +func (u *UUID) SetVersion(v byte) { + u[6] = (u[6] & 0x0f) | (v << 4) +} + +// SetVariant sets the variant bits. +func (u *UUID) SetVariant(v byte) { + switch v { + case VariantNCS: + u[8] = (u[8]&(0xff>>1) | (0x00 << 7)) + case VariantRFC9562: + u[8] = (u[8]&(0xff>>2) | (0x02 << 6)) + case VariantMicrosoft: + u[8] = (u[8]&(0xff>>3) | (0x06 << 5)) + case VariantFuture: + fallthrough + default: + u[8] = (u[8]&(0xff>>3) | (0x07 << 5)) + } +} + +// Must is a helper that wraps a call to a function returning (UUID, error) +// and panics if the error is non-nil. It is intended for use in variable +// initializations such as +// +// var packageUUID = uuid.Must(uuid.FromString("123e4567-e89b-12d3-a456-426655440000")) +func Must(u UUID, err error) UUID { + if err != nil { + panic(err) + } + return u +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md b/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md index 73fe51346..773af218e 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md +++ b/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md @@ -1,3 +1,72 @@ +## v2.9.0 (2025-11-17) + +* [GH-3508](https://github.com/gophercloud/gophercloud/pull/3508) [v2] Trigger "hold" workflow on merge groups +* [GH-3511](https://github.com/gophercloud/gophercloud/pull/3511) [v2] Closes #2321 - Fix TestRolesCRUD by including DomainID to TestRolesCRUD +* [GH-3513](https://github.com/gophercloud/gophercloud/pull/3513) [v2] build(deps): bump actions/labeler from 5 to 6 +* [GH-3516](https://github.com/gophercloud/gophercloud/pull/3516) [v2] refactor: Trivial fixes +* [GH-3524](https://github.com/gophercloud/gophercloud/pull/3524) [v2] [glance]: Add 'uploading' status +* [GH-3525](https://github.com/gophercloud/gophercloud/pull/3525) [v2] compute: Add host aggregate uuid field +* [GH-3526](https://github.com/gophercloud/gophercloud/pull/3526) [v2] Enable deletion for network and loadbalancer quotas +* [GH-3541](https://github.com/gophercloud/gophercloud/pull/3541) [v2] docs: Document tested releases for acceptance tests +* [GH-3544](https://github.com/gophercloud/gophercloud/pull/3544) [v2] Identity V3: Add Options field to roles. +* [GH-3547](https://github.com/gophercloud/gophercloud/pull/3547) [v2] Add config_drive to server struct +* [GH-3548](https://github.com/gophercloud/gophercloud/pull/3548) [v2] Identity: Add description field to roles +* [GH-3549](https://github.com/gophercloud/gophercloud/pull/3549) [v2] compute: add cpu info topology cells entry +* [GH-3550](https://github.com/gophercloud/gophercloud/pull/3550) [v2] Migrate epoxy jobs to Ubuntu 24.04 (Noble), drop caracal jobs +* [GH-3551](https://github.com/gophercloud/gophercloud/pull/3551) [v2] build(deps): bump github/codeql-action from 3 to 4 +* [GH-3557](https://github.com/gophercloud/gophercloud/pull/3557) [v2] Fix EC2 authentication to work with new Keystone auth requirement +* [GH-3558](https://github.com/gophercloud/gophercloud/pull/3558) [v2] identity/services: add omitempty to the `type` field +* [GH-3559](https://github.com/gophercloud/gophercloud/pull/3559) [v2] fix: handle Nova create image response for microversion 2.45 and above + +## v2.8.0 (2025-08-18) + +* [GH-3348](https://github.com/gophercloud/gophercloud/pull/3348) [v2] [networking] add ExtractRoutersInto func helper to routers +* [GH-3354](https://github.com/gophercloud/gophercloud/pull/3354) [v2] Fix a small typo +* [GH-3358](https://github.com/gophercloud/gophercloud/pull/3358) [v2] tests: fix devstack master branch tests +* [GH-3361](https://github.com/gophercloud/gophercloud/pull/3361) [v2] octavia: fix http_version type to float +* [GH-3362](https://github.com/gophercloud/gophercloud/pull/3362) [v2] tests: fix containerinfra template creation +* [GH-3367](https://github.com/gophercloud/gophercloud/pull/3367) [v2] Use Makefile for CI jobs +* [GH-3375](https://github.com/gophercloud/gophercloud/pull/3375) [v2] core: add missing Builder interfaces +* [GH-3378](https://github.com/gophercloud/gophercloud/pull/3378) [v2] tests: fix failing rabbitmq service +* [GH-3379](https://github.com/gophercloud/gophercloud/pull/3379) [v2] CI: Remove Bobcat +* [GH-3384](https://github.com/gophercloud/gophercloud/pull/3384) [v2] Move master CI jobs to Ubuntu 24.04 +* [GH-3386](https://github.com/gophercloud/gophercloud/pull/3386) [v2] tests: Fix TestBGPAgentCRUD +* [GH-3387](https://github.com/gophercloud/gophercloud/pull/3387) [v2] Update the doc of openstack.AuthOptionsFromEnv function +* [GH-3389](https://github.com/gophercloud/gophercloud/pull/3389) [v2] networking: add constants for statuses +* [GH-3391](https://github.com/gophercloud/gophercloud/pull/3391) [v2] CI: Add Epoxy +* [GH-3393](https://github.com/gophercloud/gophercloud/pull/3393) [v2] dns: implement shared zones list +* [GH-3394](https://github.com/gophercloud/gophercloud/pull/3394) [v2] acceptance: Prevent 409 when bulk-creating secgroup rules +* [GH-3396](https://github.com/gophercloud/gophercloud/pull/3396) [v2] identity: add support for string boolean in users' enabled member +* [GH-3397](https://github.com/gophercloud/gophercloud/pull/3397) [v2] Adjust List func to accept a Builder in tenants, routers and security groups packages +* [GH-3399](https://github.com/gophercloud/gophercloud/pull/3399) [v2] blockstorage: add manage-existing and unmanage api call +* [GH-3401](https://github.com/gophercloud/gophercloud/pull/3401) [v2] Added address groups to Networking extensions, with tests. +* [GH-3407](https://github.com/gophercloud/gophercloud/pull/3407) [v2] neutron: add segment_id support to subnets +* [GH-3413](https://github.com/gophercloud/gophercloud/pull/3413) [v2] build(deps): bump joelanford/go-apidiff from 0.8.2 to 0.8.3 +* [GH-3416](https://github.com/gophercloud/gophercloud/pull/3416) [v2] tests: bump devstack-action +* [GH-3422](https://github.com/gophercloud/gophercloud/pull/3422) [v2] Fix documentation for gateway_ip in subnet update +* [GH-3431](https://github.com/gophercloud/gophercloud/pull/3431) [v2] Use container-infra for OpenStack-API-Version +* [GH-3433](https://github.com/gophercloud/gophercloud/pull/3433) [v2] make: Use fixed version of gotestsum +* [GH-3434](https://github.com/gophercloud/gophercloud/pull/3434) [v2] Randomize test order for unit tests +* [GH-3435](https://github.com/gophercloud/gophercloud/pull/3435) [v2] Add versioned endpoint discovery +* [GH-3438](https://github.com/gophercloud/gophercloud/pull/3438) [v2] dns: add support for /v2/quotas +* [GH-3439](https://github.com/gophercloud/gophercloud/pull/3439) [v2] neutron: add segments extension package +* [GH-3446](https://github.com/gophercloud/gophercloud/pull/3446) nova: add support for hostname updates +* [GH-3452](https://github.com/gophercloud/gophercloud/pull/3452) [v2] neutron: allow omission of subnet_id for IP address +* [GH-3454](https://github.com/gophercloud/gophercloud/pull/3454) [v2] blockstorage: add isPublic query option for volume types +* [GH-3458](https://github.com/gophercloud/gophercloud/pull/3458) [v2] Fix pagination for messaging client +* [GH-3465](https://github.com/gophercloud/gophercloud/pull/3465) [v2] tests: Fix TestVLANTransparentCRUD test +* [GH-3466](https://github.com/gophercloud/gophercloud/pull/3466) [v2] tests: fix tests for recent PR backports +* [GH-3469](https://github.com/gophercloud/gophercloud/pull/3469) [v2] tests: shorten GH-A job names +* [GH-3473](https://github.com/gophercloud/gophercloud/pull/3473) [v2] core: clone service type aliases instead of referencing global slice +* [GH-3475](https://github.com/gophercloud/gophercloud/pull/3475) [v2] Implement update & delete traits on resource provider +* [GH-3476](https://github.com/gophercloud/gophercloud/pull/3476) [v2] tests: fix volumetypes unit tests +* [GH-3477](https://github.com/gophercloud/gophercloud/pull/3477) [v2] script: Improve getenvvar helper +* [GH-3481](https://github.com/gophercloud/gophercloud/pull/3481) [v2] Implement hypervisors.GetExt: Get with Query parameter +* [GH-3487](https://github.com/gophercloud/gophercloud/pull/3487) [v2] Add networking taas tapmirror suite +* [GH-3489](https://github.com/gophercloud/gophercloud/pull/3489) [v2] Fix incorrect ICMP field description in PortRangeMax comment +* [GH-3494](https://github.com/gophercloud/gophercloud/pull/3494) [v2] Networking v2: Support two time formats for subnet, router, SG rule (#3492) +* [GH-3495](https://github.com/gophercloud/gophercloud/pull/3495) [v2] build(deps): bump actions/checkout from 4 to 5 + ## v2.7.0 (2025-04-03) * [GH-3306](https://github.com/gophercloud/gophercloud/pull/3306) [v2] identity: Add Get endpoint by ID diff --git a/vendor/github.com/gophercloud/gophercloud/v2/Makefile b/vendor/github.com/gophercloud/gophercloud/v2/Makefile index 2a0618a6b..c63adb8d0 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/Makefile +++ b/vendor/github.com/gophercloud/gophercloud/v2/Makefile @@ -1,7 +1,9 @@ undefine GOFLAGS GOLANGCI_LINT_VERSION?=v1.62.2 -GO_TEST?=go run gotest.tools/gotestsum@latest --format testname -- +GOTESTSUM_VERSION?=v1.12.2 +GO_TEST?=go run gotest.tools/gotestsum@$(GOTESTSUM_VERSION) --format testname -- +TIMEOUT := "60m" ifeq ($(shell command -v podman 2> /dev/null),) RUNNER=docker @@ -9,15 +11,18 @@ else RUNNER=podman endif -# if the golangci-lint steps fails with the following error message: +# if the golangci-lint steps fails with one of the following error messages: # # directory prefix . does not contain main module or its selected dependencies # +# failed to initialize build cache at /root/.cache/golangci-lint: mkdir /root/.cache/golangci-lint: permission denied +# # you probably have to fix the SELinux security context for root directory plus your cache # # chcon -Rt svirt_sandbox_file_t . # chcon -Rt svirt_sandbox_file_t ~/.cache/golangci-lint lint: + mkdir -p ~/.cache/golangci-lint/$(GOLANGCI_LINT_VERSION) $(RUNNER) run -t --rm \ -v $(shell pwd):/app \ -v ~/.cache/golangci-lint/$(GOLANGCI_LINT_VERSION):/root/.cache \ @@ -31,84 +36,88 @@ format: .PHONY: format unit: - $(GO_TEST) ./... + $(GO_TEST) -shuffle on ./... .PHONY: unit coverage: - $(GO_TEST) -covermode count -coverprofile cover.out -coverpkg=./... ./... + $(GO_TEST) -shuffle on -covermode count -coverprofile cover.out -coverpkg=./... ./... .PHONY: coverage -acceptance: acceptance-baremetal acceptance-blockstorage acceptance-compute acceptance-container acceptance-containerinfra acceptance-db acceptance-dns acceptance-identity acceptance-imageservice acceptance-keymanager acceptance-loadbalancer acceptance-messaging acceptance-networking acceptance-objectstorage acceptance-orchestration acceptance-placement acceptance-sharedfilesystems acceptance-workflow +acceptance: acceptance-basic acceptance-baremetal acceptance-blockstorage acceptance-compute acceptance-container acceptance-containerinfra acceptance-db acceptance-dns acceptance-identity acceptance-image acceptance-keymanager acceptance-loadbalancer acceptance-messaging acceptance-networking acceptance-objectstorage acceptance-orchestration acceptance-placement acceptance-sharedfilesystems acceptance-workflow .PHONY: acceptance +acceptance-basic: + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack +.PHONY: acceptance-basic + acceptance-baremetal: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/baremetal/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/baremetal/... .PHONY: acceptance-baremetal acceptance-blockstorage: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/blockstorage/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/blockstorage/... .PHONY: acceptance-blockstorage acceptance-compute: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/compute/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/compute/... .PHONY: acceptance-compute acceptance-container: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/container/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/container/... .PHONY: acceptance-container acceptance-containerinfra: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/containerinfra/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/containerinfra/... .PHONY: acceptance-containerinfra acceptance-db: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/db/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/db/... .PHONY: acceptance-db acceptance-dns: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/dns/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/dns/... .PHONY: acceptance-dns acceptance-identity: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/identity/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/identity/... .PHONY: acceptance-identity acceptance-image: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/imageservice/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/image/... .PHONY: acceptance-image acceptance-keymanager: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/keymanager/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/keymanager/... .PHONY: acceptance-keymanager acceptance-loadbalancer: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/loadbalancer/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/loadbalancer/... .PHONY: acceptance-loadbalancer acceptance-messaging: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/messaging/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/messaging/... .PHONY: acceptance-messaging acceptance-networking: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/networking/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/networking/... .PHONY: acceptance-networking acceptance-objectstorage: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/objectstorage/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/objectstorage/... .PHONY: acceptance-objectstorage acceptance-orchestration: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/orchestration/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/orchestration/... .PHONY: acceptance-orchestration acceptance-placement: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/placement/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/placement/... .PHONY: acceptance-placement acceptance-sharedfilesystems: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/sharedfilesystems/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/sharedfilesystems/... .PHONY: acceptance-sharefilesystems acceptance-workflow: - $(GO_TEST) -tags "fixtures acceptance" ./internal/acceptance/openstack/workflow/... + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/workflow/... .PHONY: acceptance-workflow diff --git a/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go b/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go index 8818e769b..34d76a1b8 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/endpoint_search.go @@ -79,6 +79,11 @@ type EndpointOpts struct { // Required only for services that span multiple regions. Region string + // Version [optional] is the major version of the service required. It it not + // a microversion. Use this to ensure the correct endpoint is selected when + // multiple API versions are available. + Version int + // Availability [optional] is the visibility of the endpoint to be returned. // Valid types include the constants AvailabilityPublic, AvailabilityInternal, // or AvailabilityAdmin from this package. @@ -111,7 +116,7 @@ func (eo *EndpointOpts) ApplyDefaults(t string) { if len(eo.Aliases) == 0 { if aliases, ok := ServiceTypeAliases[eo.Type]; ok { // happy path: user requested a service type by its official name - eo.Aliases = aliases + eo.Aliases = slices.Clone(aliases) } else { // unhappy path: user requested a service type by its alias or an // invalid/unsupported service type @@ -121,7 +126,7 @@ func (eo *EndpointOpts) ApplyDefaults(t string) { // we intentionally override the service type, even if it // was explicitly requested by the user eo.Type = t - eo.Aliases = aliases + eo.Aliases = slices.Clone(aliases) } } } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/auth_env.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/auth_env.go new file mode 100644 index 000000000..9ecc5b4ef --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/auth_env.go @@ -0,0 +1,137 @@ +package openstack + +import ( + "os" + + "github.com/gophercloud/gophercloud/v2" +) + +var nilOptions = gophercloud.AuthOptions{} + +/* +AuthOptionsFromEnv fills out an identity.AuthOptions structure with the +settings found on the various OpenStack OS_* environment variables. + +The following variables provide sources of truth: OS_AUTH_URL, OS_USERNAME, +OS_PASSWORD and OS_PROJECT_ID. + +Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings, +or an error will result. OS_PROJECT_ID, is optional. + +OS_TENANT_ID and OS_TENANT_NAME are deprecated forms of OS_PROJECT_ID and +OS_PROJECT_NAME and the latter are expected against a v3 auth api. + +If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will still be referred +as "tenant" in Gophercloud. + +If OS_PROJECT_NAME is set, it requires OS_DOMAIN_ID or OS_DOMAIN_NAME to be +set as well to handle projects not on the default domain. + +To use this function, first set the OS_* environment variables (for example, +by sourcing an `openrc` file), then: + + opts, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(context.TODO(), opts) +*/ +func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) { + authURL := os.Getenv("OS_AUTH_URL") + username := os.Getenv("OS_USERNAME") + userID := os.Getenv("OS_USERID") + password := os.Getenv("OS_PASSWORD") + passcode := os.Getenv("OS_PASSCODE") + tenantID := os.Getenv("OS_TENANT_ID") + tenantName := os.Getenv("OS_TENANT_NAME") + domainID := os.Getenv("OS_DOMAIN_ID") + domainName := os.Getenv("OS_DOMAIN_NAME") + applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID") + applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME") + applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET") + systemScope := os.Getenv("OS_SYSTEM_SCOPE") + + // If OS_PROJECT_ID is set, overwrite tenantID with the value. + if v := os.Getenv("OS_PROJECT_ID"); v != "" { + tenantID = v + } + + // If OS_PROJECT_NAME is set, overwrite tenantName with the value. + if v := os.Getenv("OS_PROJECT_NAME"); v != "" { + tenantName = v + } + + if authURL == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_AUTH_URL", + } + return nilOptions, err + } + + if userID == "" && username == "" { + // Empty username and userID could be ignored, when applicationCredentialID and applicationCredentialSecret are set + if applicationCredentialID == "" && applicationCredentialSecret == "" { + err := gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"}, + } + return nilOptions, err + } + } + + if password == "" && passcode == "" && applicationCredentialID == "" && applicationCredentialName == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + // silently ignore TOTP passcode warning, since it is not a common auth method + EnvironmentVariable: "OS_PASSWORD", + } + return nilOptions, err + } + + if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_APPLICATION_CREDENTIAL_SECRET", + } + return nilOptions, err + } + + if domainID == "" && domainName == "" && tenantID == "" && tenantName != "" { + err := gophercloud.ErrMissingEnvironmentVariable{ + EnvironmentVariable: "OS_PROJECT_ID", + } + return nilOptions, err + } + + if applicationCredentialID == "" && applicationCredentialName != "" && applicationCredentialSecret != "" { + if userID == "" && username == "" { + return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_USERID", "OS_USERNAME"}, + } + } + if username != "" && domainID == "" && domainName == "" { + return nilOptions, gophercloud.ErrMissingAnyoneOfEnvironmentVariables{ + EnvironmentVariables: []string{"OS_DOMAIN_ID", "OS_DOMAIN_NAME"}, + } + } + } + + var scope *gophercloud.AuthScope + if systemScope == "all" { + scope = &gophercloud.AuthScope{ + System: true, + } + } + + ao := gophercloud.AuthOptions{ + IdentityEndpoint: authURL, + UserID: userID, + Username: username, + Password: password, + Passcode: passcode, + TenantID: tenantID, + TenantName: tenantName, + DomainID: domainID, + DomainName: domainName, + ApplicationCredentialID: applicationCredentialID, + ApplicationCredentialName: applicationCredentialName, + ApplicationCredentialSecret: applicationCredentialSecret, + Scope: scope, + } + + return ao, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/doc.go new file mode 100644 index 000000000..e018b57a8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/doc.go @@ -0,0 +1,168 @@ +/* +Package volumes provides information and interaction with volumes in the +OpenStack Block Storage service. A volume is a detachable block storage +device, akin to a USB hard drive. It can only be attached to one instance at +a time. + +Example of creating Volume B on a Different Host than Volume A + + schedulerHintOpts := volumes.SchedulerHintCreateOpts{ + DifferentHost: []string{ + "volume-a-uuid", + } + } + + createOpts := volumes.CreateOpts{ + Name: "volume_b", + Size: 10, + } + + volume, err := volumes.Create(context.TODO(), computeClient, createOpts, schedulerHintOpts).Extract() + if err != nil { + panic(err) + } + +Example of creating Volume B on the Same Host as Volume A + + schedulerHintOpts := volumes.SchedulerHintCreateOpts{ + SameHost: []string{ + "volume-a-uuid", + } + } + + createOpts := volumes.CreateOpts{ + Name: "volume_b", + Size: 10 + } + + volume, err := volumes.Create(context.TODO(), computeClient, createOpts, schedulerHintOpts).Extract() + if err != nil { + panic(err) + } + +Example of creating a Volume from a Backup + + backupID := "20c792f0-bb03-434f-b653-06ef238e337e" + options := volumes.CreateOpts{ + Name: "vol-001", + BackupID: &backupID, + } + + client.Microversion = "3.47" + volume, err := volumes.Create(context.TODO(), client, options, nil).Extract() + if err != nil { + panic(err) + } + + fmt.Println(volume) + +Example of Creating an Image from a Volume + + uploadImageOpts := volumes.UploadImageOpts{ + ImageName: "my_vol", + Force: true, + } + + volumeImage, err := volumes.UploadImage(context.TODO(), client, volume.ID, uploadImageOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", volumeImage) + +Example of Extending a Volume's Size + + extendOpts := volumes.ExtendSizeOpts{ + NewSize: 100, + } + + err := volumes.ExtendSize(context.TODO(), client, volume.ID, extendOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Initializing a Volume Connection + + connectOpts := &volumes.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: gophercloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + connectionInfo, err := volumes.InitializeConnection(context.TODO(), client, volume.ID, connectOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", connectionInfo["data"]) + + terminateOpts := &volumes.InitializeConnectionOpts{ + IP: "127.0.0.1", + Host: "stack", + Initiator: "iqn.1994-05.com.redhat:17cf566367d2", + Multipath: gophercloud.Disabled, + Platform: "x86_64", + OSType: "linux2", + } + + err = volumes.TerminateConnection(context.TODO(), client, volume.ID, terminateOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Setting a Volume's Bootable status + + options := volumes.BootableOpts{ + Bootable: true, + } + + err := volumes.SetBootable(context.TODO(), client, volume.ID, options).ExtractErr() + if err != nil { + panic(err) + } + +Example of Changing Type of a Volume + + changeTypeOpts := volumes.ChangeTypeOpts{ + NewType: "ssd", + MigrationPolicy: volumes.MigrationPolicyOnDemand, + } + + err = volumes.ChangeType(context.TODO(), client, volumeID, changeTypeOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Attaching a Volume to an Instance + + attachOpts := volumes.AttachOpts{ + MountPoint: "/mnt", + Mode: "rw", + InstanceUUID: server.ID, + } + + err := volumes.Attach(context.TODO(), client, volume.ID, attachOpts).ExtractErr() + if err != nil { + panic(err) + } + + detachOpts := volumes.DetachOpts{ + AttachmentID: volume.Attachments[0].AttachmentID, + } + + err = volumes.Detach(context.TODO(), client, volume.ID, detachOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example of Unmanaging a Volume + + err := volumes.Unmanage(context.TODO(), client, volume.ID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package volumes diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/requests.go new file mode 100644 index 000000000..1026d1eca --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/requests.go @@ -0,0 +1,788 @@ +package volumes + +import ( + "context" + "maps" + "regexp" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// SchedulerHintOptsBuilder builds the scheduler hints into a serializable format. +type SchedulerHintOptsBuilder interface { + ToSchedulerHintsMap() (map[string]any, error) +} + +// SchedulerHintOpts contains options for providing scheduler hints +// when creating a Volume. This object is passed to the volumes.Create function. +// For more information about these parameters, see the Volume object. +type SchedulerHintOpts struct { + // DifferentHost will place the volume on a different back-end that does not + // host the given volumes. + DifferentHost []string + + // SameHost will place the volume on a back-end that hosts the given volumes. + SameHost []string + + // LocalToInstance will place volume on same host on a given instance + LocalToInstance string + + // Query is a conditional statement that results in back-ends able to + // host the volume. + Query string + + // AdditionalProperies are arbitrary key/values that are not validated by nova. + AdditionalProperties map[string]any +} + +// ToSchedulerHintsMap assembles a request body for scheduler hints +func (opts SchedulerHintOpts) ToSchedulerHintsMap() (map[string]any, error) { + sh := make(map[string]any) + + uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$") + + if len(opts.DifferentHost) > 0 { + for _, diffHost := range opts.DifferentHost { + if !uuidRegex.MatchString(diffHost) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "volumes.SchedulerHintOpts.DifferentHost" + err.Value = opts.DifferentHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["different_host"] = opts.DifferentHost + } + + if len(opts.SameHost) > 0 { + for _, sameHost := range opts.SameHost { + if !uuidRegex.MatchString(sameHost) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "volumes.SchedulerHintOpts.SameHost" + err.Value = opts.SameHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["same_host"] = opts.SameHost + } + + if opts.LocalToInstance != "" { + if !uuidRegex.MatchString(opts.LocalToInstance) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "volumes.SchedulerHintOpts.LocalToInstance" + err.Value = opts.LocalToInstance + err.Info = "The instance must be in UUID format." + return nil, err + } + sh["local_to_instance"] = opts.LocalToInstance + } + + if opts.Query != "" { + sh["query"] = opts.Query + } + + if opts.AdditionalProperties != nil { + for k, v := range opts.AdditionalProperties { + sh[k] = v + } + } + + if len(sh) == 0 { + return sh, nil + } + + return map[string]any{"OS-SCH-HNT:scheduler_hints": sh}, nil +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToVolumeCreateMap() (map[string]any, error) +} + +// CreateOpts contains options for creating a Volume. This object is passed to +// the volumes.Create function. For more information about these parameters, +// see the Volume object. +type CreateOpts struct { + // The size of the volume, in GB + Size int `json:"size,omitempty"` + // The availability zone + AvailabilityZone string `json:"availability_zone,omitempty"` + // ConsistencyGroupID is the ID of a consistency group + ConsistencyGroupID string `json:"consistencygroup_id,omitempty"` + // The volume description + Description string `json:"description,omitempty"` + // One or more metadata key and value pairs to associate with the volume + Metadata map[string]string `json:"metadata,omitempty"` + // The volume name + Name string `json:"name,omitempty"` + // the ID of the existing volume snapshot + SnapshotID string `json:"snapshot_id,omitempty"` + // SourceReplica is a UUID of an existing volume to replicate with + SourceReplica string `json:"source_replica,omitempty"` + // the ID of the existing volume + SourceVolID string `json:"source_volid,omitempty"` + // The ID of the image from which you want to create the volume. + // Required to create a bootable volume. + ImageID string `json:"imageRef,omitempty"` + // Specifies the backup ID, from which you want to create the volume. + // Create a volume from a backup is supported since 3.47 microversion + BackupID string `json:"backup_id,omitempty"` + // The associated volume type + VolumeType string `json:"volume_type,omitempty"` +} + +// ToVolumeCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToVolumeCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Create will create a new Volume based on the values in CreateOpts. To extract +// the Volume object from the response, call the Extract method on the +// CreateResult. +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder, hintOpts SchedulerHintOptsBuilder) (r CreateResult) { + b, err := opts.ToVolumeCreateMap() + if err != nil { + r.Err = err + return + } + + if hintOpts != nil { + sh, err := hintOpts.ToSchedulerHintsMap() + if err != nil { + r.Err = err + return + } + maps.Copy(b, sh) + } + + resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToVolumeDeleteQuery() (string, error) +} + +// DeleteOpts contains options for deleting a Volume. This object is passed to +// the volumes.Delete function. +type DeleteOpts struct { + // Delete all snapshots of this volume as well. + Cascade bool `q:"cascade"` +} + +// ToLoadBalancerDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToVolumeDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete will delete the existing Volume with the provided ID. +func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) { + url := deleteURL(client, id) + if opts != nil { + query, err := opts.ToVolumeDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + resp, err := client.Delete(ctx, url, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves the Volume with the provided ID. To extract the Volume object +// from the response, call the Extract method on the GetResult. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToVolumeListQuery() (string, error) +} + +// ListOpts holds options for listing Volumes. It is passed to the volumes.List +// function. +type ListOpts struct { + // AllTenants will retrieve volumes of all tenants/projects. + AllTenants bool `q:"all_tenants"` + + // Metadata will filter results based on specified metadata. + Metadata map[string]string `q:"metadata"` + + // Name will filter by the specified volume name. + Name string `q:"name"` + + // Status will filter by the specified status. + Status string `q:"status"` + + // TenantID will filter by a specific tenant/project ID. + // Setting AllTenants is required for this. + TenantID string `q:"project_id"` + + // Comma-separated list of sort keys and optional sort directions in the + // form of [:]. + Sort string `q:"sort"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // Used in conjunction with limit to return a slice of items. + Offset int `q:"offset"` + + // The ID of the last-seen item. + Marker string `q:"marker"` + + // Bootable will filter results based on whether they are bootable volumes + Bootable *bool `q:"bootable,omitempty"` +} + +// ToVolumeListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToVolumeListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns Volumes optionally limited by the conditions provided in ListOpts. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToVolumeListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return VolumePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToVolumeUpdateMap() (map[string]any, error) +} + +// UpdateOpts contain options for updating an existing Volume. This object is passed +// to the volumes.Update function. For more information about the parameters, see +// the Volume object. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToVolumeUpdateMap assembles a request body based on the contents of an +// UpdateOpts. +func (opts UpdateOpts) ToVolumeUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "volume") +} + +// Update will update the Volume with provided information. To extract the updated +// Volume from the response, call the Extract method on the UpdateResult. +func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToVolumeUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// AttachOptsBuilder allows extensions to add additional parameters to the +// Attach request. +type AttachOptsBuilder interface { + ToVolumeAttachMap() (map[string]any, error) +} + +// AttachMode describes the attachment mode for volumes. +type AttachMode string + +// These constants determine how a volume is attached. +const ( + ReadOnly AttachMode = "ro" + ReadWrite AttachMode = "rw" +) + +// AttachOpts contains options for attaching a Volume. +type AttachOpts struct { + // The mountpoint of this volume. + MountPoint string `json:"mountpoint,omitempty"` + + // The nova instance ID, can't set simultaneously with HostName. + InstanceUUID string `json:"instance_uuid,omitempty"` + + // The hostname of baremetal host, can't set simultaneously with InstanceUUID. + HostName string `json:"host_name,omitempty"` + + // Mount mode of this volume. + Mode AttachMode `json:"mode,omitempty"` +} + +// ToVolumeAttachMap assembles a request body based on the contents of a +// AttachOpts. +func (opts AttachOpts) ToVolumeAttachMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-attach") +} + +// Attach will attach a volume based on the values in AttachOpts. +func Attach(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AttachOptsBuilder) (r AttachResult) { + b, err := opts.ToVolumeAttachMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// BeginDetaching will mark the volume as detaching. +func BeginDetaching(ctx context.Context, client *gophercloud.ServiceClient, id string) (r BeginDetachingResult) { + b := map[string]any{"os-begin_detaching": make(map[string]any)} + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DetachOptsBuilder allows extensions to add additional parameters to the +// Detach request. +type DetachOptsBuilder interface { + ToVolumeDetachMap() (map[string]any, error) +} + +// DetachOpts contains options for detaching a Volume. +type DetachOpts struct { + // AttachmentID is the ID of the attachment between a volume and instance. + AttachmentID string `json:"attachment_id,omitempty"` +} + +// ToVolumeDetachMap assembles a request body based on the contents of a +// DetachOpts. +func (opts DetachOpts) ToVolumeDetachMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-detach") +} + +// Detach will detach a volume based on volume ID. +func Detach(ctx context.Context, client *gophercloud.ServiceClient, id string, opts DetachOptsBuilder) (r DetachResult) { + b, err := opts.ToVolumeDetachMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Reserve will reserve a volume based on volume ID. +func Reserve(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ReserveResult) { + b := map[string]any{"os-reserve": make(map[string]any)} + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Unreserve will unreserve a volume based on volume ID. +func Unreserve(ctx context.Context, client *gophercloud.ServiceClient, id string) (r UnreserveResult) { + b := map[string]any{"os-unreserve": make(map[string]any)} + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// InitializeConnectionOptsBuilder allows extensions to add additional parameters to the +// InitializeConnection request. +type InitializeConnectionOptsBuilder interface { + ToVolumeInitializeConnectionMap() (map[string]any, error) +} + +// InitializeConnectionOpts hosts options for InitializeConnection. +// The fields are specific to the storage driver in use and the destination +// attachment. +type InitializeConnectionOpts struct { + IP string `json:"ip,omitempty"` + Host string `json:"host,omitempty"` + Initiator string `json:"initiator,omitempty"` + Wwpns []string `json:"wwpns,omitempty"` + Wwnns string `json:"wwnns,omitempty"` + Multipath *bool `json:"multipath,omitempty"` + Platform string `json:"platform,omitempty"` + OSType string `json:"os_type,omitempty"` +} + +// ToVolumeInitializeConnectionMap assembles a request body based on the contents of a +// InitializeConnectionOpts. +func (opts InitializeConnectionOpts) ToVolumeInitializeConnectionMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "connector") + return map[string]any{"os-initialize_connection": b}, err +} + +// InitializeConnection initializes an iSCSI connection by volume ID. +func InitializeConnection(ctx context.Context, client *gophercloud.ServiceClient, id string, opts InitializeConnectionOptsBuilder) (r InitializeConnectionResult) { + b, err := opts.ToVolumeInitializeConnectionMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// TerminateConnectionOptsBuilder allows extensions to add additional parameters to the +// TerminateConnection request. +type TerminateConnectionOptsBuilder interface { + ToVolumeTerminateConnectionMap() (map[string]any, error) +} + +// TerminateConnectionOpts hosts options for TerminateConnection. +type TerminateConnectionOpts struct { + IP string `json:"ip,omitempty"` + Host string `json:"host,omitempty"` + Initiator string `json:"initiator,omitempty"` + Wwpns []string `json:"wwpns,omitempty"` + Wwnns string `json:"wwnns,omitempty"` + Multipath *bool `json:"multipath,omitempty"` + Platform string `json:"platform,omitempty"` + OSType string `json:"os_type,omitempty"` +} + +// ToVolumeTerminateConnectionMap assembles a request body based on the contents of a +// TerminateConnectionOpts. +func (opts TerminateConnectionOpts) ToVolumeTerminateConnectionMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "connector") + return map[string]any{"os-terminate_connection": b}, err +} + +// TerminateConnection terminates an iSCSI connection by volume ID. +func TerminateConnection(ctx context.Context, client *gophercloud.ServiceClient, id string, opts TerminateConnectionOptsBuilder) (r TerminateConnectionResult) { + b, err := opts.ToVolumeTerminateConnectionMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ExtendSizeOptsBuilder allows extensions to add additional parameters to the +// ExtendSize request. +type ExtendSizeOptsBuilder interface { + ToVolumeExtendSizeMap() (map[string]any, error) +} + +// ExtendSizeOpts contains options for extending the size of an existing Volume. +// This object is passed to the volumes.ExtendSize function. +type ExtendSizeOpts struct { + // NewSize is the new size of the volume, in GB. + NewSize int `json:"new_size" required:"true"` +} + +// ToVolumeExtendSizeMap assembles a request body based on the contents of an +// ExtendSizeOpts. +func (opts ExtendSizeOpts) ToVolumeExtendSizeMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-extend") +} + +// ExtendSize will extend the size of the volume based on the provided information. +// This operation does not return a response body. +func ExtendSize(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ExtendSizeOptsBuilder) (r ExtendSizeResult) { + b, err := opts.ToVolumeExtendSizeMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UploadImageOptsBuilder allows extensions to add additional parameters to the +// UploadImage request. +type UploadImageOptsBuilder interface { + ToVolumeUploadImageMap() (map[string]any, error) +} + +// UploadImageOpts contains options for uploading a Volume to image storage. +type UploadImageOpts struct { + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format,omitempty"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format,omitempty"` + + // The name of image that will be stored in glance. + ImageName string `json:"image_name,omitempty"` + + // Force image creation, usable if volume attached to instance. + Force bool `json:"force,omitempty"` + + // Visibility defines who can see/use the image. + // supported since 3.1 microversion + Visibility string `json:"visibility,omitempty"` + + // whether the image is not deletable. + // supported since 3.1 microversion + Protected bool `json:"protected,omitempty"` +} + +// ToVolumeUploadImageMap assembles a request body based on the contents of a +// UploadImageOpts. +func (opts UploadImageOpts) ToVolumeUploadImageMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-volume_upload_image") +} + +// UploadImage will upload an image based on the values in UploadImageOptsBuilder. +func UploadImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UploadImageOptsBuilder) (r UploadImageResult) { + b, err := opts.ToVolumeUploadImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ForceDelete will delete the volume regardless of state. +func ForceDelete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ForceDeleteResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"os-force_delete": ""}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ImageMetadataOptsBuilder allows extensions to add additional parameters to the +// ImageMetadataRequest request. +type ImageMetadataOptsBuilder interface { + ToImageMetadataMap() (map[string]any, error) +} + +// ImageMetadataOpts contains options for setting image metadata to a volume. +type ImageMetadataOpts struct { + // The image metadata to add to the volume as a set of metadata key and value pairs. + Metadata map[string]string `json:"metadata"` +} + +// ToImageMetadataMap assembles a request body based on the contents of a +// ImageMetadataOpts. +func (opts ImageMetadataOpts) ToImageMetadataMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-set_image_metadata") +} + +// SetImageMetadata will set image metadata on a volume based on the values in ImageMetadataOptsBuilder. +func SetImageMetadata(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ImageMetadataOptsBuilder) (r SetImageMetadataResult) { + b, err := opts.ToImageMetadataMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// BootableOptsBuilder allows extensions to add additional parameters to the +// SetBootable request. +type BootableOptsBuilder interface { + ToBootableMap() (map[string]any, error) +} + +// BootableOpts contains options for setting bootable status to a volume. +type BootableOpts struct { + // Enables or disables the bootable attribute. You can boot an instance from a bootable volume. + Bootable bool `json:"bootable"` +} + +// ToBootableMap assembles a request body based on the contents of a +// BootableOpts. +func (opts BootableOpts) ToBootableMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-set_bootable") +} + +// SetBootable will set bootable status on a volume based on the values in BootableOpts +func SetBootable(ctx context.Context, client *gophercloud.ServiceClient, id string, opts BootableOptsBuilder) (r SetBootableResult) { + b, err := opts.ToBootableMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// MigrationPolicy type represents a migration_policy when changing types. +type MigrationPolicy string + +// Supported attributes for MigrationPolicy attribute for changeType operations. +const ( + MigrationPolicyNever MigrationPolicy = "never" + MigrationPolicyOnDemand MigrationPolicy = "on-demand" +) + +// ChangeTypeOptsBuilder allows extensions to add additional parameters to the +// ChangeType request. +type ChangeTypeOptsBuilder interface { + ToVolumeChangeTypeMap() (map[string]any, error) +} + +// ChangeTypeOpts contains options for changing the type of an existing Volume. +// This object is passed to the volumes.ChangeType function. +type ChangeTypeOpts struct { + // NewType is the name of the new volume type of the volume. + NewType string `json:"new_type" required:"true"` + + // MigrationPolicy specifies if the volume should be migrated when it is + // re-typed. Possible values are "on-demand" or "never". If not specified, + // the default is "never". + MigrationPolicy MigrationPolicy `json:"migration_policy,omitempty"` +} + +// ToVolumeChangeTypeMap assembles a request body based on the contents of an +// ChangeTypeOpts. +func (opts ChangeTypeOpts) ToVolumeChangeTypeMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-retype") +} + +// ChangeType will change the volume type of the volume based on the provided information. +// This operation does not return a response body. +func ChangeType(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ChangeTypeOptsBuilder) (r ChangeTypeResult) { + b, err := opts.ToVolumeChangeTypeMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ReImageOptsBuilder allows extensions to add additional parameters to the +// ReImage request. +type ReImageOptsBuilder interface { + ToReImageMap() (map[string]any, error) +} + +// ReImageOpts contains options for Re-image a volume. +type ReImageOpts struct { + // New image id + ImageID string `json:"image_id"` + // set true to re-image volumes in reserved state + ReImageReserved bool `json:"reimage_reserved"` +} + +// ToReImageMap assembles a request body based on the contents of a ReImageOpts. +func (opts ReImageOpts) ToReImageMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-reimage") +} + +// ReImage will re-image a volume based on the values in ReImageOpts +func ReImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ReImageOptsBuilder) (r ReImageResult) { + b, err := opts.ToReImageMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResetStatusOptsBuilder allows extensions to add additional parameters to the +// ResetStatus request. +type ResetStatusOptsBuilder interface { + ToResetStatusMap() (map[string]any, error) +} + +// ResetStatusOpts contains options for resetting a Volume status. +// For more information about these parameters, please, refer to the Block Storage API V3, +// Volume Actions, ResetStatus volume documentation. +type ResetStatusOpts struct { + // Status is a volume status to reset to. + Status string `json:"status"` + // MigrationStatus is a volume migration status to reset to. + MigrationStatus string `json:"migration_status,omitempty"` + // AttachStatus is a volume attach status to reset to. + AttachStatus string `json:"attach_status,omitempty"` +} + +// ToResetStatusMap assembles a request body based on the contents of a +// ResetStatusOpts. +func (opts ResetStatusOpts) ToResetStatusMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-reset_status") +} + +// ResetStatus will reset the existing volume status. ResetStatusResult contains only the error. +// To extract it, call the ExtractErr method on the ResetStatusResult. +func ResetStatus(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ResetStatusOptsBuilder) (r ResetStatusResult) { + b, err := opts.ToResetStatusMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Unmanage removes a volume from Block Storage management without +// removing the back-end storage object that is associated with it. +func Unmanage(ctx context.Context, client *gophercloud.ServiceClient, id string) (r UnmanageResult) { + body := map[string]any{"os-unmanage": make(map[string]any)} + resp, err := client.Post(ctx, actionURL(client, id), body, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/results.go new file mode 100644 index 000000000..e99ef5e19 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/results.go @@ -0,0 +1,406 @@ +package volumes + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Attachment represents a Volume Attachment record +type Attachment struct { + AttachedAt time.Time `json:"-"` + AttachmentID string `json:"attachment_id"` + Device string `json:"device"` + HostName string `json:"host_name"` + ID string `json:"id"` + ServerID string `json:"server_id"` + VolumeID string `json:"volume_id"` +} + +// UnmarshalJSON is our unmarshalling helper +func (r *Attachment) UnmarshalJSON(b []byte) error { + type tmp Attachment + var s struct { + tmp + AttachedAt gophercloud.JSONRFC3339MilliNoZ `json:"attached_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Attachment(s.tmp) + + r.AttachedAt = time.Time(s.AttachedAt) + + return err +} + +// Volume contains all the information associated with an OpenStack Volume. +type Volume struct { + // Unique identifier for the volume. + ID string `json:"id"` + // Current status of the volume. + Status string `json:"status"` + // Size of the volume in GB. + Size int `json:"size"` + // AvailabilityZone is which availability zone the volume is in. + AvailabilityZone string `json:"availability_zone"` + // The date when this volume was created. + CreatedAt time.Time `json:"-"` + // The date when this volume was last updated + UpdatedAt time.Time `json:"-"` + // Instances onto which the volume is attached. + Attachments []Attachment `json:"attachments"` + // Human-readable display name for the volume. + Name string `json:"name"` + // Human-readable description for the volume. + Description string `json:"description"` + // The type of volume to create, either SATA or SSD. + VolumeType string `json:"volume_type"` + // The ID of the snapshot from which the volume was created + SnapshotID string `json:"snapshot_id"` + // The ID of another block storage volume from which the current volume was created + SourceVolID string `json:"source_volid"` + // The backup ID, from which the volume was restored + // This field is supported since 3.47 microversion + BackupID *string `json:"backup_id"` + // Arbitrary key-value pairs defined by the user. + Metadata map[string]string `json:"metadata"` + // UserID is the id of the user who created the volume. + UserID string `json:"user_id"` + // Indicates whether this is a bootable volume. + Bootable string `json:"bootable"` + // Encrypted denotes if the volume is encrypted. + Encrypted bool `json:"encrypted"` + // ReplicationStatus is the status of replication. + ReplicationStatus string `json:"replication_status"` + // ConsistencyGroupID is the consistency group ID. + ConsistencyGroupID string `json:"consistencygroup_id"` + // Multiattach denotes if the volume is multi-attach capable. + Multiattach bool `json:"multiattach"` + // Image metadata entries, only included for volumes that were created from an image, or from a snapshot of a volume originally created from an image. + VolumeImageMetadata map[string]string `json:"volume_image_metadata"` + // Host is the identifier of the host holding the volume. + Host string `json:"os-vol-host-attr:host"` + // TenantID is the id of the project that owns the volume. + TenantID string `json:"os-vol-tenant-attr:tenant_id"` +} + +// UnmarshalJSON another unmarshalling function +func (r *Volume) UnmarshalJSON(b []byte) error { + type tmp Volume + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// VolumePage is a pagination.pager that is returned from a call to the List function. +type VolumePage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a ListResult contains no Volumes. +func (r VolumePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + volumes, err := ExtractVolumes(r) + return len(volumes) == 0, err +} + +func (page VolumePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"volumes_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractVolumes extracts and returns Volumes. It is used while iterating over a volumes.List call. +func ExtractVolumes(r pagination.Page) ([]Volume, error) { + var s []Volume + err := ExtractVolumesInto(r, &s) + return s, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will get the Volume object out of the commonResult object. +func (r commonResult) Extract() (*Volume, error) { + var s Volume + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractInto converts our response data into a volume struct +func (r commonResult) ExtractInto(v any) error { + return r.Result.ExtractIntoStructPtr(v, "volume") +} + +// ExtractVolumesInto similar to ExtractInto but operates on a `list` of volumes +func ExtractVolumesInto(r pagination.Page, v any) error { + return r.(VolumePage).Result.ExtractIntoSlicePtr(v, "volumes") +} + +// CreateResult contains the response body and error from a Create request. +type CreateResult struct { + commonResult +} + +// GetResult contains the response body and error from a Get request. +type GetResult struct { + commonResult +} + +// UpdateResult contains the response body and error from an Update request. +type UpdateResult struct { + commonResult +} + +// DeleteResult contains the response body and error from a Delete request. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AttachResult contains the response body and error from an Attach request. +type AttachResult struct { + gophercloud.ErrResult +} + +// BeginDetachingResult contains the response body and error from a BeginDetach +// request. +type BeginDetachingResult struct { + gophercloud.ErrResult +} + +// DetachResult contains the response body and error from a Detach request. +type DetachResult struct { + gophercloud.ErrResult +} + +// UploadImageResult contains the response body and error from an UploadImage +// request. +type UploadImageResult struct { + gophercloud.Result +} + +// SetImageMetadataResult contains the response body and error from an SetImageMetadata +// request. +type SetImageMetadataResult struct { + gophercloud.ErrResult +} + +// SetBootableResult contains the response body and error from a SetBootable +// request. +type SetBootableResult struct { + gophercloud.ErrResult +} + +// ReserveResult contains the response body and error from a Reserve request. +type ReserveResult struct { + gophercloud.ErrResult +} + +// UnreserveResult contains the response body and error from an Unreserve +// request. +type UnreserveResult struct { + gophercloud.ErrResult +} + +// TerminateConnectionResult contains the response body and error from a +// TerminateConnection request. +type TerminateConnectionResult struct { + gophercloud.ErrResult +} + +// InitializeConnectionResult contains the response body and error from an +// InitializeConnection request. +type InitializeConnectionResult struct { + gophercloud.Result +} + +// ExtendSizeResult contains the response body and error from an ExtendSize request. +type ExtendSizeResult struct { + gophercloud.ErrResult +} + +// Extract will get the connection information out of the +// InitializeConnectionResult object. +// +// This will be a generic map[string]any and the results will be +// dependent on the type of connection made. +func (r InitializeConnectionResult) Extract() (map[string]any, error) { + var s struct { + ConnectionInfo map[string]any `json:"connection_info"` + } + err := r.ExtractInto(&s) + return s.ConnectionInfo, err +} + +// ImageVolumeType contains volume type information obtained from UploadImage +// action. +type ImageVolumeType struct { + // The ID of a volume type. + ID string `json:"id"` + + // Human-readable display name for the volume type. + Name string `json:"name"` + + // Human-readable description for the volume type. + Description string `json:"display_description"` + + // Flag for public access. + IsPublic bool `json:"is_public"` + + // Extra specifications for volume type. + ExtraSpecs map[string]any `json:"extra_specs"` + + // ID of quality of service specs. + QosSpecsID string `json:"qos_specs_id"` + + // Flag for deletion status of volume type. + Deleted bool `json:"deleted"` + + // The date when volume type was deleted. + DeletedAt time.Time `json:"-"` + + // The date when volume type was created. + CreatedAt time.Time `json:"-"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` +} + +func (r *ImageVolumeType) UnmarshalJSON(b []byte) error { + type tmp ImageVolumeType + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339MilliNoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + DeletedAt gophercloud.JSONRFC3339MilliNoZ `json:"deleted_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ImageVolumeType(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + r.DeletedAt = time.Time(s.DeletedAt) + + return err +} + +// VolumeImage contains information about volume uploaded to an image service. +type VolumeImage struct { + // The ID of a volume an image is created from. + VolumeID string `json:"id"` + + // Container format, may be bare, ofv, ova, etc. + ContainerFormat string `json:"container_format"` + + // Disk format, may be raw, qcow2, vhd, vdi, vmdk, etc. + DiskFormat string `json:"disk_format"` + + // Human-readable description for the volume. + Description string `json:"display_description"` + + // The ID of the created image. + ImageID string `json:"image_id"` + + // Human-readable display name for the image. + ImageName string `json:"image_name"` + + // Size of the volume in GB. + Size int `json:"size"` + + // Current status of the volume. + Status string `json:"status"` + + // Visibility defines who can see/use the image. + // supported since 3.1 microversion + Visibility string `json:"visibility"` + + // whether the image is not deletable. + // supported since 3.1 microversion + Protected bool `json:"protected"` + + // The date when this volume was last updated. + UpdatedAt time.Time `json:"-"` + + // Volume type object of used volume. + VolumeType ImageVolumeType `json:"volume_type"` +} + +func (r *VolumeImage) UnmarshalJSON(b []byte) error { + type tmp VolumeImage + var s struct { + tmp + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = VolumeImage(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return err +} + +// Extract will get an object with info about the uploaded image out of the +// UploadImageResult object. +func (r UploadImageResult) Extract() (VolumeImage, error) { + var s struct { + VolumeImage VolumeImage `json:"os-volume_upload_image"` + } + err := r.ExtractInto(&s) + return s.VolumeImage, err +} + +// ForceDeleteResult contains the response body and error from a ForceDelete request. +type ForceDeleteResult struct { + gophercloud.ErrResult +} + +// ChangeTypeResult contains the response body and error from an ChangeType request. +type ChangeTypeResult struct { + gophercloud.ErrResult +} + +// ReImageResult contains the response body and error from a ReImage request. +type ReImageResult struct { + gophercloud.ErrResult +} + +// ResetStatusResult contains the response error from a ResetStatus request. +type ResetStatusResult struct { + gophercloud.ErrResult +} + +// UnmanageResult contains the response error from a Unmanage request. +type UnmanageResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/urls.go new file mode 100644 index 000000000..a73e2d2b1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/urls.go @@ -0,0 +1,27 @@ +package volumes + +import "github.com/gophercloud/gophercloud/v2" + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes") +} + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("volumes", "detail") +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return deleteURL(c, id) +} + +func actionURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("volumes", id, "action") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/util.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/util.go new file mode 100644 index 000000000..6f8d899f5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes/util.go @@ -0,0 +1,23 @@ +package volumes + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" +) + +// WaitForStatus will continually poll the resource, checking for a particular status. +func WaitForStatus(ctx context.Context, c *gophercloud.ServiceClient, id, status string) error { + return gophercloud.WaitFor(ctx, func(ctx context.Context) (bool, error) { + current, err := Get(ctx, c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go new file mode 100644 index 000000000..73ca5c56d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go @@ -0,0 +1,502 @@ +package openstack + +import ( + "context" + "errors" + "fmt" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud/v2" + tokens2 "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1" + tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/v2/openstack/utils" +) + +const ( + // v2 represents Keystone v2. + // It should never increase beyond 2.0. + v2 = "v2.0" + + // v3 represents Keystone v3. + // The version can be anything from v3 to v3.x. + v3 = "v3" +) + +// NewClient prepares an unauthenticated ProviderClient instance. +// Most users will probably prefer using the AuthenticatedClient function +// instead. +// +// This is useful if you wish to explicitly control the version of the identity +// service that's used for authentication explicitly, for example. +// +// A basic example of using this would be: +// +// ao, err := openstack.AuthOptionsFromEnv() +// provider, err := openstack.NewClient(ao.IdentityEndpoint) +// client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{}) +func NewClient(endpoint string) (*gophercloud.ProviderClient, error) { + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + endpoint = gophercloud.NormalizeURL(endpoint) + base = gophercloud.NormalizeURL(base) + + p := new(gophercloud.ProviderClient) + p.IdentityBase = base + p.IdentityEndpoint = endpoint + p.UseTokenLock() + + return p, nil +} + +// AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint +// specified by the options, acquires a token, and returns a Provider Client +// instance that's ready to operate. +// +// If the full path to a versioned identity endpoint was specified (example: +// http://example.com:5000/v3), that path will be used as the endpoint to query. +// +// If a versionless endpoint was specified (example: http://example.com:5000/), +// the endpoint will be queried to determine which versions of the identity service +// are available, then chooses the most recent or most supported version. +// +// Example: +// +// ao, err := openstack.AuthOptionsFromEnv() +// provider, err := openstack.AuthenticatedClient(ctx, ao) +// client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ +// Region: os.Getenv("OS_REGION_NAME"), +// }) +func AuthenticatedClient(ctx context.Context, options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) { + client, err := NewClient(options.IdentityEndpoint) + if err != nil { + return nil, err + } + + err = Authenticate(ctx, client, options) + if err != nil { + return nil, err + } + return client, nil +} + +// Authenticate authenticates or re-authenticates against the most +// recent identity service supported at the provided endpoint. +func Authenticate(ctx context.Context, client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error { + versions := []*utils.Version{ + {ID: v2, Priority: 20, Suffix: "/v2.0/"}, + {ID: v3, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(ctx, client, versions) + if err != nil { + return err + } + + switch chosen.ID { + case v2: + return v2auth(ctx, client, endpoint, &options, gophercloud.EndpointOpts{}) + case v3: + return v3auth(ctx, client, endpoint, &options, gophercloud.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("Unrecognized identity version: %s", chosen.ID) + } +} + +// AuthenticateV2 explicitly authenticates against the identity v2 endpoint. +func AuthenticateV2(ctx context.Context, client *gophercloud.ProviderClient, options tokens2.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + return v2auth(ctx, client, "", options, eo) +} + +type v2TokenNoReauth struct { + tokens2.AuthOptionsBuilder +} + +func (v2TokenNoReauth) CanReauth() bool { return false } + +func v2auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint string, options tokens2.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + v2Client, err := NewIdentityV2(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v2Client.Endpoint = endpoint + } + + result := tokens2.Create(ctx, v2Client, options) + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + if options.CanReauth() { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.SetThrowaway(true) + tac.ReauthFunc = nil + err := tac.SetTokenAndAuthResult(nil) + if err != nil { + return err + } + client.ReauthFunc = func(ctx context.Context) error { + err := v2auth(ctx, &tac, endpoint, &v2TokenNoReauth{options}, eo) + if err != nil { + return err + } + client.CopyTokenFrom(&tac) + return nil + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V2Endpoint(context.TODO(), client, catalog, opts) + } + + return nil +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(ctx context.Context, client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + return v3auth(ctx, client, "", options, eo) +} + +func v3auth(ctx context.Context, client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error { + // Override the generated service endpoint with the one returned by the version endpoint. + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + var catalog *tokens3.ServiceCatalog + + var tokenID string + // passthroughToken allows to passthrough the token without a scope + var passthroughToken bool + switch v := opts.(type) { + case *gophercloud.AuthOptions: + tokenID = v.TokenID + passthroughToken = (v.Scope == nil || *v.Scope == gophercloud.AuthScope{}) + case *tokens3.AuthOptions: + tokenID = v.TokenID + passthroughToken = (v.Scope == tokens3.Scope{}) + } + + if tokenID != "" && passthroughToken { + // passing through the token ID without requesting a new scope + if opts.CanReauth() { + return fmt.Errorf("cannot use AllowReauth, when the token ID is defined and auth scope is not set") + } + + v3Client.SetToken(tokenID) + result := tokens3.Get(ctx, v3Client, tokenID) + if result.Err != nil { + return result.Err + } + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err = result.ExtractServiceCatalog() + if err != nil { + return err + } + } else { + var result tokens3.CreateResult + switch opts.(type) { + case *ec2tokens.AuthOptions: + result = ec2tokens.Create(ctx, v3Client, opts) + case *oauth1.AuthOptions: + result = oauth1.Create(ctx, v3Client, opts) + default: + result = tokens3.Create(ctx, v3Client, opts) + } + + err = client.SetTokenAndAuthResult(result) + if err != nil { + return err + } + + catalog, err = result.ExtractServiceCatalog() + if err != nil { + return err + } + } + + if opts.CanReauth() { + // here we're creating a throw-away client (tac). it's a copy of the user's provider client, but + // with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`, + // this should retry authentication only once + tac := *client + tac.SetThrowaway(true) + tac.ReauthFunc = nil + err = tac.SetTokenAndAuthResult(nil) + if err != nil { + return err + } + var tao tokens3.AuthOptionsBuilder + switch ot := opts.(type) { + case *gophercloud.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *tokens3.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *ec2tokens.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + case *oauth1.AuthOptions: + o := *ot + o.AllowReauth = false + tao = &o + default: + tao = opts + } + client.ReauthFunc = func(ctx context.Context) error { + err := v3auth(ctx, &tac, endpoint, tao, eo) + if err != nil { + return err + } + client.CopyTokenFrom(&tac) + return nil + } + } + client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) { + return V3Endpoint(context.TODO(), client, catalog, opts) + } + + return nil +} + +// NewIdentityV2 creates a ServiceClient that may be used to interact with the +// v2 identity service. +func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v2.0/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 +// identity service. +func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) { + eo.ApplyDefaults(clientType) + endpoint, err = client.EndpointLocator(eo) + if err != nil { + return nil, err + } + } + + // Ensure endpoint still has a suffix of v3. + // This is because EndpointLocator might have found a versionless + // endpoint or the published endpoint is still /v2.0. In both + // cases, we need to fix the endpoint to point to /v3. + base, err := utils.BaseEndpoint(endpoint) + if err != nil { + return nil, err + } + + base = gophercloud.NormalizeURL(base) + + endpoint = base + "v3/" + + return &gophercloud.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +// TODO(stephenfin): Allow passing aliases to all New${SERVICE}V${VERSION} methods in v3 +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string, version int) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + + eo.ApplyDefaults(clientType) + if eo.Version != 0 && eo.Version != version { + return sc, errors.New("Conflict between requested service major version and manually set version") + } + eo.Version = version + + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewBareMetalV1 creates a ServiceClient that may be used with the v1 +// bare metal package. +func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "baremetal", 1) + if !strings.HasSuffix(strings.TrimSuffix(sc.Endpoint, "/"), "v1") { + sc.ResourceBase = sc.Endpoint + "v1/" + } + return sc, err +} + +// NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1 +// bare metal introspection package. +func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "baremetal-introspection", 1) +} + +// NewObjectStorageV1 creates a ServiceClient that may be used with the v1 +// object storage package. +func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "object-store", 1) +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute +// package. +func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "compute", 2) +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network +// package. +func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network", 2) + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// TODO(stephenfin): Remove this in v3. We no longer support the V1 Block Storage service. +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 +// block storage service. +func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "volume", 1) +} + +// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 +// block storage service. +func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "block-storage", 2) +} + +// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service. +func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "block-storage", 3) +} + +// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. +func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "shared-file-system", 2) +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 +// orchestration service. +func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "orchestration", 1) +} + +// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service. +func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "database", 1) +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS +// service. +func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dns", 2) + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewImageV2 creates a ServiceClient that may be used to access the v2 image +// service. +func NewImageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image", 2) + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2 +// load balancer service. +func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "load-balancer", 2) + + // Fixes edge case having an OpenStack lb endpoint with trailing version number. + endpoint := strings.Replace(sc.Endpoint, "v2.0/", "", -1) + + sc.ResourceBase = endpoint + "v2.0/" + return sc, err +} + +// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging +// service. +func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "message", 2) + sc.MoreHeaders = map[string]string{"Client-ID": clientID} + return sc, err +} + +// NewContainerV1 creates a ServiceClient that may be used with v1 container package +func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "application-container", 1) +} + +// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key +// manager service. +func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "key-manager", 1) + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} + +// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management +// package. +func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "container-infrastructure-management", 1) +} + +// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package. +func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "workflow", 2) +} + +// NewPlacementV1 creates a ServiceClient that may be used with the placement package. +func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + return initClientOpts(client, eo, "placement", 1) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/doc.go new file mode 100644 index 000000000..e07dd0f8f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/doc.go @@ -0,0 +1,51 @@ +/* +Package extensions provides information and interaction with the different +extensions available for an OpenStack service. + +The purpose of OpenStack API extensions is to: + +- Introduce new features in the API without requiring a version change. +- Introduce vendor-specific niche functionality. +- Act as a proving ground for experimental functionalities that might be +included in a future version of the API. + +Extensions usually have tags that prevent conflicts with other extensions that +define attributes or resources with the same names, and with core resources and +attributes. Because an extension might not be supported by all plug-ins, its +availability varies with deployments and the specific plug-in. + +The results of this package vary depending on the type of Service Client used. +In the following examples, note how the only difference is the creation of the +Service Client. + +Example of Retrieving Compute Extensions + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(context.TODO(), ao) + computeClient, err := openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + + allPages, err := extensions.List(computeClient).AllPages(context.TODO()) + allExtensions, err := extensions.ExtractExtensions(allPages) + + for _, extension := range allExtensions{ + fmt.Printf("%+v\n", extension) + } + +Example of Retrieving Network Extensions + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(context.TODO(), ao) + networkClient, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + + allPages, err := extensions.List(networkClient).AllPages(context.TODO()) + allExtensions, err := extensions.ExtractExtensions(allPages) + + for _, extension := range allExtensions{ + fmt.Printf("%+v\n", extension) + } +*/ +package extensions diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/requests.go new file mode 100644 index 000000000..40e18f357 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/requests.go @@ -0,0 +1,23 @@ +package extensions + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Get retrieves information for a specific extension using its alias. +func Get(ctx context.Context, c *gophercloud.ServiceClient, alias string) (r GetResult) { + resp, err := c.Get(ctx, ExtensionURL(c, alias), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, ListExtensionURL(c), func(r pagination.PageResult) pagination.Page { + return ExtensionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/results.go new file mode 100644 index 000000000..0db049ad0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/results.go @@ -0,0 +1,57 @@ +package extensions + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// GetResult temporarily stores the result of a Get call. +// Use its Extract() method to interpret it as an Extension. +type GetResult struct { + gophercloud.Result +} + +// Extract interprets a GetResult as an Extension. +func (r GetResult) Extract() (*Extension, error) { + var s struct { + Extension *Extension `json:"extension"` + } + err := r.ExtractInto(&s) + return s.Extension, err +} + +// Extension is a struct that represents an OpenStack extension. +type Extension struct { + Updated string `json:"updated"` + Name string `json:"name"` + Links []any `json:"links"` + Namespace string `json:"namespace"` + Alias string `json:"alias"` + Description string `json:"description"` +} + +// ExtensionPage is the page returned by a pager when traversing over a collection of extensions. +type ExtensionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an ExtensionPage struct is empty. +func (r ExtensionPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractExtensions(r) + return len(is) == 0, err +} + +// ExtractExtensions accepts a Page struct, specifically an ExtensionPage +// struct, and extracts the elements into a slice of Extension structs. +// In other words, a generic collection is mapped into a relevant slice. +func ExtractExtensions(r pagination.Page) ([]Extension, error) { + var s struct { + Extensions []Extension `json:"extensions"` + } + err := (r.(ExtensionPage)).ExtractInto(&s) + return s.Extensions, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/urls.go new file mode 100644 index 000000000..8eb5f5372 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/common/extensions/urls.go @@ -0,0 +1,13 @@ +package extensions + +import "github.com/gophercloud/gophercloud/v2" + +// ExtensionURL generates the URL for an extension resource by name. +func ExtensionURL(c *gophercloud.ServiceClient, name string) string { + return c.ServiceURL("extensions", name) +} + +// ListExtensionURL generates the URL for the extensions resource collection. +func ListExtensionURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("extensions") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/doc.go new file mode 100644 index 000000000..3b1ad9fb5 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/doc.go @@ -0,0 +1,52 @@ +/* +Package attachinterfaces provides the ability to retrieve and manage network +interfaces through Nova. + +Example of Listing a Server's Interfaces + + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + allPages, err := attachinterfaces.List(computeClient, serverID).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allInterfaces, err := attachinterfaces.ExtractInterfaces(allPages) + if err != nil { + panic(err) + } + + for _, interface := range allInterfaces { + fmt.Printf("%+v\n", interface) + } + +Example to Get a Server's Interface + + portID = "0dde1598-b374-474e-986f-5b8dd1df1d4e" + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + interface, err := attachinterfaces.Get(context.TODO(), computeClient, serverID, portID).Extract() + if err != nil { + panic(err) + } + +Example to Create a new Interface attachment on the Server + + networkID := "8a5fe506-7e9f-4091-899b-96336909d93c" + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + attachOpts := attachinterfaces.CreateOpts{ + NetworkID: networkID, + } + interface, err := attachinterfaces.Create(context.TODO(), computeClient, serverID, attachOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Interface attachment from the Server + + portID = "0dde1598-b374-474e-986f-5b8dd1df1d4e" + serverID := "b07e7a3b-d951-4efc-a4f9-ac9f001afb7f" + err := attachinterfaces.Delete(context.TODO(), computeClient, serverID, portID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package attachinterfaces diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/requests.go new file mode 100644 index 000000000..9be0ac68c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/requests.go @@ -0,0 +1,77 @@ +package attachinterfaces + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// List makes a request against the nova API to list the server's interfaces. +func List(client *gophercloud.ServiceClient, serverID string) pagination.Pager { + return pagination.NewPager(client, listInterfaceURL(client, serverID), func(r pagination.PageResult) pagination.Page { + return InterfacePage{pagination.SinglePageBase(r)} + }) +} + +// Get requests details on a single interface attachment by the server and port IDs. +func Get(ctx context.Context, client *gophercloud.ServiceClient, serverID, portID string) (r GetResult) { + resp, err := client.Get(ctx, getInterfaceURL(client, serverID, portID), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToAttachInterfacesCreateMap() (map[string]any, error) +} + +// CreateOpts specifies parameters of a new interface attachment. +type CreateOpts struct { + // PortID is the ID of the port for which you want to create an interface. + // The NetworkID and PortID parameters are mutually exclusive. + // If you do not specify the PortID parameter, the OpenStack Networking API + // v2.0 allocates a port and creates an interface for it on the network. + PortID string `json:"port_id,omitempty"` + + // NetworkID is the ID of the network for which you want to create an interface. + // The NetworkID and PortID parameters are mutually exclusive. + // If you do not specify the NetworkID parameter, the OpenStack Networking + // API v2.0 uses the network information cache that is associated with the instance. + NetworkID string `json:"net_id,omitempty"` + + // Slice of FixedIPs. If you request a specific FixedIP address without a + // NetworkID, the request returns a Bad Request (400) response code. + // Note: this uses the FixedIP struct, but only the IPAddress field can be used. + FixedIPs []FixedIP `json:"fixed_ips,omitempty"` +} + +// ToAttachInterfacesCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToAttachInterfacesCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "interfaceAttachment") +} + +// Create requests the creation of a new interface attachment on the server. +func Create(ctx context.Context, client *gophercloud.ServiceClient, serverID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToAttachInterfacesCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, createInterfaceURL(client, serverID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete makes a request against the nova API to detach a single interface from the server. +// It needs server and port IDs to make a such request. +func Delete(ctx context.Context, client *gophercloud.ServiceClient, serverID, portID string) (r DeleteResult) { + resp, err := client.Delete(ctx, deleteInterfaceURL(client, serverID, portID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/results.go new file mode 100644 index 000000000..6895be69d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/results.go @@ -0,0 +1,84 @@ +package attachinterfaces + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type attachInterfaceResult struct { + gophercloud.Result +} + +// Extract interprets any attachInterfaceResult as an Interface, if possible. +func (r attachInterfaceResult) Extract() (*Interface, error) { + var s struct { + Interface *Interface `json:"interfaceAttachment"` + } + err := r.ExtractInto(&s) + return s.Interface, err +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as an Interface. +type GetResult struct { + attachInterfaceResult +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as an Interface. +type CreateResult struct { + attachInterfaceResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FixedIP represents a Fixed IP Address. +// This struct is also used when creating an attachment, +// but it is not possible to specify a SubnetID. +type FixedIP struct { + SubnetID string `json:"subnet_id,omitempty"` + IPAddress string `json:"ip_address"` +} + +// Interface represents a network interface on a server. +type Interface struct { + PortState string `json:"port_state"` + FixedIPs []FixedIP `json:"fixed_ips"` + PortID string `json:"port_id"` + NetID string `json:"net_id"` + MACAddr string `json:"mac_addr"` +} + +// InterfacePage abstracts the raw results of making a List() request against +// the API. +// +// As OpenStack extensions may freely alter the response bodies of structures +// returned to the client, you may only safely access the data provided through +// the ExtractInterfaces call. +type InterfacePage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an InterfacePage contains no interfaces. +func (r InterfacePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + interfaces, err := ExtractInterfaces(r) + return len(interfaces) == 0, err +} + +// ExtractInterfaces interprets the results of a single page from a List() call, +// producing a slice of Interface structs. +func ExtractInterfaces(r pagination.Page) ([]Interface, error) { + var s struct { + Interfaces []Interface `json:"interfaceAttachments"` + } + err := (r.(InterfacePage)).ExtractInto(&s) + return s.Interfaces, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/urls.go new file mode 100644 index 000000000..98f8271d2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces/urls.go @@ -0,0 +1,18 @@ +package attachinterfaces + +import "github.com/gophercloud/gophercloud/v2" + +func listInterfaceURL(client *gophercloud.ServiceClient, serverID string) string { + return client.ServiceURL("servers", serverID, "os-interface") +} + +func getInterfaceURL(client *gophercloud.ServiceClient, serverID, portID string) string { + return client.ServiceURL("servers", serverID, "os-interface", portID) +} + +func createInterfaceURL(client *gophercloud.ServiceClient, serverID string) string { + return client.ServiceURL("servers", serverID, "os-interface") +} +func deleteInterfaceURL(client *gophercloud.ServiceClient, serverID, portID string) string { + return client.ServiceURL("servers", serverID, "os-interface", portID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/doc.go new file mode 100644 index 000000000..59278fda3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/doc.go @@ -0,0 +1,38 @@ +/* +Package availabilityzones provides the ability to get lists and detailed +availability zone information and to extend a server result with +availability zone information. + +Example of Get Availability Zone Information + + allPages, err := availabilityzones.List(computeClient).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } + +Example of Get Detailed Availability Zone Information + + allPages, err := availabilityzones.ListDetail(computeClient).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + availabilityZoneInfo, err := availabilityzones.ExtractAvailabilityZones(allPages) + if err != nil { + panic(err) + } + + for _, zoneInfo := range availabilityZoneInfo { + fmt.Printf("%+v\n", zoneInfo) + } +*/ +package availabilityzones diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/requests.go new file mode 100644 index 000000000..319e61978 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/requests.go @@ -0,0 +1,20 @@ +package availabilityzones + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// List will return the existing availability zones. +func List(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} + +// ListDetail will return the existing availability zones with detailed information. +func ListDetail(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, listDetailURL(client), func(r pagination.PageResult) pagination.Page { + return AvailabilityZonePage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/results.go new file mode 100644 index 000000000..3c95a8a24 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/results.go @@ -0,0 +1,70 @@ +package availabilityzones + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ServiceState represents the state of a service in an AvailabilityZone. +type ServiceState struct { + Active bool `json:"active"` + Available bool `json:"available"` + UpdatedAt time.Time `json:"-"` +} + +// UnmarshalJSON to override default +func (r *ServiceState) UnmarshalJSON(b []byte) error { + type tmp ServiceState + var s struct { + tmp + UpdatedAt gophercloud.JSONRFC3339MilliNoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = ServiceState(s.tmp) + + r.UpdatedAt = time.Time(s.UpdatedAt) + + return nil +} + +// Services is a map of services contained in an AvailabilityZone. +type Services map[string]ServiceState + +// Hosts is map of hosts/nodes contained in an AvailabilityZone. +// Each host can have multiple services. +type Hosts map[string]Services + +// ZoneState represents the current state of the availability zone. +type ZoneState struct { + // Returns true if the availability zone is available + Available bool `json:"available"` +} + +// AvailabilityZone contains all the information associated with an OpenStack +// AvailabilityZone. +type AvailabilityZone struct { + Hosts Hosts `json:"hosts"` + // The availability zone name + ZoneName string `json:"zoneName"` + ZoneState ZoneState `json:"zoneState"` +} + +type AvailabilityZonePage struct { + pagination.SinglePageBase +} + +// ExtractAvailabilityZones returns a slice of AvailabilityZones contained in a +// single page of results. +func ExtractAvailabilityZones(r pagination.Page) ([]AvailabilityZone, error) { + var s struct { + AvailabilityZoneInfo []AvailabilityZone `json:"availabilityZoneInfo"` + } + err := (r.(AvailabilityZonePage)).ExtractInto(&s) + return s.AvailabilityZoneInfo, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/urls.go new file mode 100644 index 000000000..88c748275 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones/urls.go @@ -0,0 +1,11 @@ +package availabilityzones + +import "github.com/gophercloud/gophercloud/v2" + +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone") +} + +func listDetailURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("os-availability-zone", "detail") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/doc.go new file mode 100644 index 000000000..28a5870c2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/doc.go @@ -0,0 +1,150 @@ +/* +Package flavors provides information and interaction with the flavor API +in the OpenStack Compute service. + +A flavor is an available hardware configuration for a server. Each flavor +has a unique combination of disk space, memory capacity and priority for CPU +time. + +Example to List Flavors + + listOpts := flavors.ListOpts{ + AccessType: flavors.PublicAccess, + } + + allPages, err := flavors.ListDetail(computeClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v\n", flavor) + } + +Example to Create a Flavor + + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: gophercloud.IntToPointer(1), + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + flavor, err := flavors.Create(context.TODO(), computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.UpdateOpts{ + Description: "This is a good description" + } + + flavor, err := flavors.Update(context.TODO(), computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to List Flavor Access + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + allPages, err := flavors.ListAccesses(computeClient, flavorID).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allAccesses, err := flavors.ExtractAccesses(allPages) + if err != nil { + panic(err) + } + + for _, access := range allAccesses { + fmt.Printf("%+v", access) + } + +Example to Grant Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.AddAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.AddAccess(context.TODO(), computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove/Revoke Access to a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + accessOpts := flavors.RemoveAccessOpts{ + Tenant: "15153a0979884b59b0592248ef947921", + } + + accessList, err := flavors.RemoveAccess(context.TODO(), computeClient, flavor.ID, accessOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + createOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_policy": "CPU-POLICY", + "hw:cpu_thread_policy": "CPU-THREAD-POLICY", + } + createdExtraSpecs, err := flavors.CreateExtraSpecs(context.TODO(), computeClient, flavorID, createOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", createdExtraSpecs) + +Example to Get Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + extraSpecs, err := flavors.ListExtraSpecs(context.TODO(), computeClient, flavorID).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", extraSpecs) + +Example to Update Extra Specs for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + + updateOpts := flavors.ExtraSpecsOpts{ + "hw:cpu_thread_policy": "CPU-THREAD-POLICY-UPDATED", + } + updatedExtraSpec, err := flavors.UpdateExtraSpec(context.TODO(), computeClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v", updatedExtraSpec) + +Example to Delete an Extra Spec for a Flavor + + flavorID := "e91758d6-a54a-4778-ad72-0c73a1cb695b" + err := flavors.DeleteExtraSpec(context.TODO(), computeClient, flavorID, "hw:cpu_thread_policy").ExtractErr() + if err != nil { + panic(err) + } +*/ +package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/requests.go new file mode 100644 index 000000000..119f5e78e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/requests.go @@ -0,0 +1,366 @@ +package flavors + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +/* +AccessType maps to OpenStack's Flavor.is_public field. Although the is_public +field is boolean, the request options are ternary, which is why AccessType is +a string. The following values are allowed: + +The AccessType arguement is optional, and if it is not supplied, OpenStack +returns the PublicAccess flavors. +*/ +type AccessType string + +const ( + // PublicAccess returns public flavors and private flavors associated with + // that project. + PublicAccess AccessType = "true" + + // PrivateAccess (admin only) returns private flavors, across all projects. + PrivateAccess AccessType = "false" + + // AllAccess (admin only) returns public and private flavors across all + // projects. + AllAccess AccessType = "None" +) + +/* +ListOpts filters the results returned by the List() function. +For example, a flavor with a minDisk field of 10 will not be returned if you +specify MinDisk set to 20. + +Typically, software will use the last ID of the previous call to List to set +the Marker for the current call. +*/ +type ListOpts struct { + // ChangesSince, if provided, instructs List to return only those things which + // have changed since the timestamp provided. + ChangesSince string `q:"changes-since"` + + // MinDisk and MinRAM, if provided, elides flavors which do not meet your + // criteria. + MinDisk int `q:"minDisk"` + MinRAM int `q:"minRam"` + + // SortDir allows to select sort direction. + // It can be "asc" or "desc" (default). + SortDir string `q:"sort_dir"` + + // SortKey allows to sort by one of the flavors attributes. + // Default is flavorid. + SortKey string `q:"sort_key"` + + // Marker and Limit control paging. + // Marker instructs List where to start listing from. + Marker string `q:"marker"` + + // Limit instructs List to refrain from sending excessively large lists of + // flavors. + Limit int `q:"limit"` + + // AccessType, if provided, instructs List which set of flavors to return. + // If IsPublic not provided, flavors for the current project are returned. + AccessType AccessType `q:"is_public"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListDetail instructs OpenStack to provide a list of flavors. +// You may provide criteria by which List curtails its results for easier +// processing. +func ListDetail(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type CreateOptsBuilder interface { + ToFlavorCreateMap() (map[string]any, error) +} + +// CreateOpts specifies parameters used for creating a flavor. +type CreateOpts struct { + // Name is the name of the flavor. + Name string `json:"name" required:"true"` + + // RAM is the memory of the flavor, measured in MB. + RAM int `json:"ram" required:"true"` + + // VCPUs is the number of vcpus for the flavor. + VCPUs int `json:"vcpus" required:"true"` + + // Disk the amount of root disk space, measured in GB. + Disk *int `json:"disk" required:"true"` + + // ID is a unique ID for the flavor. + ID string `json:"id,omitempty"` + + // Swap is the amount of swap space for the flavor, measured in MB. + Swap *int `json:"swap,omitempty"` + + // RxTxFactor alters the network bandwidth of a flavor. + RxTxFactor float64 `json:"rxtx_factor,omitempty"` + + // IsPublic flags a flavor as being available to all projects or not. + IsPublic *bool `json:"os-flavor-access:is_public,omitempty"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral *int `json:"OS-FLV-EXT-DATA:ephemeral,omitempty"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` +} + +// ToFlavorCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToFlavorCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Create requests the creation of a new flavor. +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFlavorCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +type UpdateOptsBuilder interface { + ToFlavorUpdateMap() (map[string]any, error) +} + +// UpdateOpts specifies parameters used for updating a flavor. +type UpdateOpts struct { + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description,omitempty"` +} + +// ToFlavorUpdateMap constructs a request body from UpdateOpts. +func (opts UpdateOpts) ToFlavorUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Update requests the update of a new flavor. +func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFlavorUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves details of a single flavor. Use Extract to convert its +// result into a Flavor. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete deletes the specified flavor ID. +func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(ctx, deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAccesses retrieves the tenants which have access to a flavor. +func ListAccesses(client *gophercloud.ServiceClient, id string) pagination.Pager { + url := accessURL(client, id) + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessPage{pagination.SinglePageBase(r)} + }) +} + +// AddAccessOptsBuilder allows extensions to add additional parameters to the +// AddAccess requests. +type AddAccessOptsBuilder interface { + ToFlavorAddAccessMap() (map[string]any, error) +} + +// AddAccessOpts represents options for adding access to a flavor. +type AddAccessOpts struct { + // Tenant is the project/tenant ID to grant access. + Tenant string `json:"tenant"` +} + +// ToFlavorAddAccessMap constructs a request body from AddAccessOpts. +func (opts AddAccessOpts) ToFlavorAddAccessMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "addTenantAccess") +} + +// AddAccess grants a tenant/project access to a flavor. +func AddAccess(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AddAccessOptsBuilder) (r AddAccessResult) { + b, err := opts.ToFlavorAddAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveAccessOptsBuilder allows extensions to add additional parameters to the +// RemoveAccess requests. +type RemoveAccessOptsBuilder interface { + ToFlavorRemoveAccessMap() (map[string]any, error) +} + +// RemoveAccessOpts represents options for removing access to a flavor. +type RemoveAccessOpts struct { + // Tenant is the project/tenant ID to grant access. + Tenant string `json:"tenant"` +} + +// ToFlavorRemoveAccessMap constructs a request body from RemoveAccessOpts. +func (opts RemoveAccessOpts) ToFlavorRemoveAccessMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "removeTenantAccess") +} + +// RemoveAccess removes/revokes a tenant/project access to a flavor. +func RemoveAccess(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RemoveAccessOptsBuilder) (r RemoveAccessResult) { + b, err := opts.ToFlavorRemoveAccessMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, accessActionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ExtraSpecs requests all the extra-specs for the given flavor ID. +func ListExtraSpecs(ctx context.Context, client *gophercloud.ServiceClient, flavorID string) (r ListExtraSpecsResult) { + resp, err := client.Get(ctx, extraSpecsListURL(client, flavorID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +func GetExtraSpec(ctx context.Context, client *gophercloud.ServiceClient, flavorID string, key string) (r GetExtraSpecResult) { + resp, err := client.Get(ctx, extraSpecsGetURL(client, flavorID, key), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateExtraSpecsOptsBuilder allows extensions to add additional parameters to the +// CreateExtraSpecs requests. +type CreateExtraSpecsOptsBuilder interface { + ToFlavorExtraSpecsCreateMap() (map[string]any, error) +} + +// ExtraSpecsOpts is a map that contains key-value pairs. +type ExtraSpecsOpts map[string]string + +// ToFlavorExtraSpecsCreateMap assembles a body for a Create request based on +// the contents of ExtraSpecsOpts. +func (opts ExtraSpecsOpts) ToFlavorExtraSpecsCreateMap() (map[string]any, error) { + return map[string]any{"extra_specs": opts}, nil +} + +// CreateExtraSpecs will create or update the extra-specs key-value pairs for +// the specified Flavor. +func CreateExtraSpecs(ctx context.Context, client *gophercloud.ServiceClient, flavorID string, opts CreateExtraSpecsOptsBuilder) (r CreateExtraSpecsResult) { + b, err := opts.ToFlavorExtraSpecsCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, extraSpecsCreateURL(client, flavorID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateExtraSpecOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateExtraSpecOptsBuilder interface { + ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) +} + +// ToFlavorExtraSpecUpdateMap assembles a body for an Update request based on +// the contents of a ExtraSpecOpts. +func (opts ExtraSpecsOpts) ToFlavorExtraSpecUpdateMap() (map[string]string, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "flavors.ExtraSpecOpts" + err.Info = "Must have 1 and only one key-value pair" + return nil, "", err + } + + var key string + for k := range opts { + key = k + } + + return opts, key, nil +} + +// UpdateExtraSpec will updates the value of the specified flavor's extra spec +// for the key in opts. +func UpdateExtraSpec(ctx context.Context, client *gophercloud.ServiceClient, flavorID string, opts UpdateExtraSpecOptsBuilder) (r UpdateExtraSpecResult) { + b, key, err := opts.ToFlavorExtraSpecUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, extraSpecUpdateURL(client, flavorID, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteExtraSpec will delete the key-value pair with the given key for the given +// flavor ID. +func DeleteExtraSpec(ctx context.Context, client *gophercloud.ServiceClient, flavorID, key string) (r DeleteExtraSpecResult) { + resp, err := client.Delete(ctx, extraSpecDeleteURL(client, flavorID, key), &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/results.go new file mode 100644 index 000000000..387268af8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/results.go @@ -0,0 +1,277 @@ +package flavors + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// CreateResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type CreateResult struct { + commonResult +} + +// UpdateResult is the response of a Put operation. Call its Extract method to +// interpret it as a Flavor. +type UpdateResult struct { + commonResult +} + +// GetResult is the response of a Get operations. Call its Extract method to +// interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// DeleteResult is the result from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Extract provides access to the individual Flavor returned by the Get and +// Create functions. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// Flavor represent (virtual) hardware configurations for server resources +// in a region. +type Flavor struct { + // ID is the flavor's unique ID. + ID string `json:"id"` + + // Disk is the amount of root disk, measured in GB. + Disk int `json:"disk"` + + // RAM is the amount of memory, measured in MB. + RAM int `json:"ram"` + + // Name is the name of the flavor. + Name string `json:"name"` + + // RxTxFactor describes bandwidth alterations of the flavor. + RxTxFactor float64 `json:"rxtx_factor"` + + // Swap is the amount of swap space, measured in MB. + Swap int `json:"-"` + + // VCPUs indicates how many (virtual) CPUs are available for this flavor. + VCPUs int `json:"vcpus"` + + // IsPublic indicates whether the flavor is public. + IsPublic bool `json:"os-flavor-access:is_public"` + + // Ephemeral is the amount of ephemeral disk space, measured in GB. + Ephemeral int `json:"OS-FLV-EXT-DATA:ephemeral"` + + // Description is a free form description of the flavor. Limited to + // 65535 characters in length. Only printable characters are allowed. + // New in version 2.55 + Description string `json:"description"` + + // Properties is a dictionary of the flavor’s extra-specs key-and-value + // pairs. This will only be included if the user is allowed by policy to + // index flavor extra_specs + // New in version 2.61 + ExtraSpecs map[string]string `json:"extra_specs"` +} + +func (r *Flavor) UnmarshalJSON(b []byte) error { + type tmp Flavor + var s struct { + tmp + Swap any `json:"swap"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Flavor(s.tmp) + + switch t := s.Swap.(type) { + case float64: + r.Swap = int(t) + case string: + switch t { + case "": + r.Swap = 0 + default: + swap, err := strconv.ParseFloat(t, 64) + if err != nil { + return err + } + r.Swap = int(swap) + } + } + + return nil +} + +// FlavorPage contains a single page of all flavors from a ListDetails call. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines if a FlavorPage contains any results. +func (page FlavorPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + flavors, err := ExtractFlavors(page) + return len(flavors) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (page FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"flavors_links"` + } + err := page.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractFlavors provides access to the list of flavors in a page acquired +// from the ListDetail operation. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +// AccessPage contains a single page of all FlavorAccess entries for a flavor. +type AccessPage struct { + pagination.SinglePageBase +} + +// IsEmpty indicates whether an AccessPage is empty. +func (page AccessPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + v, err := ExtractAccesses(page) + return len(v) == 0, err +} + +// ExtractAccesses interprets a page of results as a slice of FlavorAccess. +func ExtractAccesses(r pagination.Page) ([]FlavorAccess, error) { + var s struct { + FlavorAccesses []FlavorAccess `json:"flavor_access"` + } + err := (r.(AccessPage)).ExtractInto(&s) + return s.FlavorAccesses, err +} + +type accessResult struct { + gophercloud.Result +} + +// AddAccessResult is the response of an AddAccess operation. Call its +// Extract method to interpret it as a slice of FlavorAccess. +type AddAccessResult struct { + accessResult +} + +// RemoveAccessResult is the response of a RemoveAccess operation. Call its +// Extract method to interpret it as a slice of FlavorAccess. +type RemoveAccessResult struct { + accessResult +} + +// Extract provides access to the result of an access create or delete. +// The result will be all accesses that the flavor has. +func (r accessResult) Extract() ([]FlavorAccess, error) { + var s struct { + FlavorAccesses []FlavorAccess `json:"flavor_access"` + } + err := r.ExtractInto(&s) + return s.FlavorAccesses, err +} + +// FlavorAccess represents an ACL of tenant access to a specific Flavor. +type FlavorAccess struct { + // FlavorID is the unique ID of the flavor. + FlavorID string `json:"flavor_id"` + + // TenantID is the unique ID of the tenant. + TenantID string `json:"tenant_id"` +} + +// Extract interprets any extraSpecsResult as ExtraSpecs, if possible. +func (r extraSpecsResult) Extract() (map[string]string, error) { + var s struct { + ExtraSpecs map[string]string `json:"extra_specs"` + } + err := r.ExtractInto(&s) + return s.ExtraSpecs, err +} + +// extraSpecsResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type extraSpecsResult struct { + gophercloud.Result +} + +// ListExtraSpecsResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type ListExtraSpecsResult struct { + extraSpecsResult +} + +// CreateExtraSpecResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateExtraSpecsResult struct { + extraSpecsResult +} + +// extraSpecResult contains the result of a call for individual a single +// key-value pair. +type extraSpecResult struct { + gophercloud.Result +} + +// GetExtraSpecResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetExtraSpecResult struct { + extraSpecResult +} + +// UpdateExtraSpecResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateExtraSpecResult struct { + extraSpecResult +} + +// DeleteExtraSpecResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteExtraSpecResult struct { + gophercloud.ErrResult +} + +// Extract interprets any extraSpecResult as an ExtraSpec, if possible. +func (r extraSpecResult) Extract() (map[string]string, error) { + var s map[string]string + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/urls.go new file mode 100644 index 000000000..27ca0571f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors/urls.go @@ -0,0 +1,53 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud/v2" +) + +func getURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors", "detail") +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("flavors") +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id) +} + +func accessURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-flavor-access") +} + +func accessActionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "action") +} + +func extraSpecsListURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecsGetURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecsCreateURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("flavors", id, "os-extra_specs") +} + +func extraSpecUpdateURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} + +func extraSpecDeleteURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("flavors", id, "os-extra_specs", key) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/doc.go new file mode 100644 index 000000000..063851ed7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/doc.go @@ -0,0 +1,58 @@ +/* +Package servergroups provides the ability to manage server groups. + +Example to List Server Groups + + allpages, err := servergroups.List(computeClient).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allServerGroups, err := servergroups.ExtractServerGroups(allPages) + if err != nil { + panic(err) + } + + for _, sg := range allServerGroups { + fmt.Printf("%#v\n", sg) + } + +Example to Create a Server Group + + createOpts := servergroups.CreateOpts{ + Name: "my_sg", + Policies: []string{"anti-affinity"}, + } + + sg, err := servergroups.Create(context.TODO(), computeClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Server Group with additional microversion 2.64 fields + + createOpts := servergroups.CreateOpts{ + Name: "my_sg", + Policy: "anti-affinity", + Rules: &servergroups.Rules{ + MaxServerPerHost: 3, + }, + } + + computeClient.Microversion = "2.64" + result := servergroups.Create(context.TODO(), computeClient, createOpts) + + serverGroup, err := result.Extract() + if err != nil { + panic(err) + } + +Example to Delete a Server Group + + sgID := "7a6f29ad-e34d-4368-951a-58a08f11cfb7" + err := servergroups.Delete(context.TODO(), computeClient, sgID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package servergroups diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/requests.go new file mode 100644 index 000000000..c2b840001 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/requests.go @@ -0,0 +1,102 @@ +package servergroups + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +type ListOpts struct { + // AllProjects is a bool to show all projects. + AllProjects bool `q:"all_projects"` + + // Requests a page size of items. + Limit int `q:"limit"` + + // Used in conjunction with limit to return a slice of items. + Offset int `q:"offset"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager that allows you to iterate over a collection of +// ServerGroups. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerGroupPage{pagination.SinglePageBase(r)} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerGroupCreateMap() (map[string]any, error) +} + +// CreateOpts specifies Server Group creation parameters. +type CreateOpts struct { + // Name is the name of the server group. + Name string `json:"name" required:"true"` + + // Policies are the server group policies. + Policies []string `json:"policies,omitempty"` + + // Policy specifies the name of a policy. + // Requires microversion 2.64 or later. + Policy string `json:"policy,omitempty"` + + // Rules specifies the set of rules. + // Requires microversion 2.64 or later. + Rules *Rules `json:"rules,omitempty"` +} + +// ToServerGroupCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToServerGroupCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "server_group") +} + +// Create requests the creation of a new Server Group. +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToServerGroupCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get returns data about a previously created ServerGroup. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete requests the deletion of a previously allocated ServerGroup. +func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(ctx, deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/results.go new file mode 100644 index 000000000..1d97dcc9e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/results.go @@ -0,0 +1,115 @@ +package servergroups + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// A ServerGroup creates a policy for instance placement in the cloud. +// You should use extract methods from microversions.go to retrieve additional +// fields. +type ServerGroup struct { + // ID is the unique ID of the Server Group. + ID string `json:"id"` + + // Name is the common name of the server group. + Name string `json:"name"` + + // Polices are the group policies. + // + // Normally a single policy is applied: + // + // "affinity" will place all servers within the server group on the + // same compute node. + // + // "anti-affinity" will place servers within the server group on different + // compute nodes. + Policies []string `json:"policies"` + + // Members are the members of the server group. + Members []string `json:"members"` + + // UserID of the server group. + UserID string `json:"user_id"` + + // ProjectID of the server group. + ProjectID string `json:"project_id"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the Server Group. + Metadata map[string]any + + // Policy is the policy of a server group. + // This requires microversion 2.64 or later. + Policy *string `json:"policy"` + + // Rules are the rules of the server group. + // This requires microversion 2.64 or later. + Rules *Rules `json:"rules"` +} + +// Rules represents set of rules for a policy. +// This requires microversion 2.64 or later. +type Rules struct { + // MaxServerPerHost specifies how many servers can reside on a single compute host. + // It can be used only with the "anti-affinity" policy. + MaxServerPerHost int `json:"max_server_per_host"` +} + +// ServerGroupPage stores a single page of all ServerGroups results from a +// List call. +type ServerGroupPage struct { + pagination.SinglePageBase +} + +// IsEmpty determines whether or not a ServerGroupsPage is empty. +func (page ServerGroupPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + va, err := ExtractServerGroups(page) + return len(va) == 0, err +} + +// ExtractServerGroups interprets a page of results as a slice of +// ServerGroups. +func ExtractServerGroups(r pagination.Page) ([]ServerGroup, error) { + var s struct { + ServerGroups []ServerGroup `json:"server_groups"` + } + err := (r.(ServerGroupPage)).ExtractInto(&s) + return s.ServerGroups, err +} + +type ServerGroupResult struct { + gophercloud.Result +} + +// Extract is a method that attempts to interpret any Server Group resource +// response as a ServerGroup struct. +func (r ServerGroupResult) Extract() (*ServerGroup, error) { + var s struct { + ServerGroup *ServerGroup `json:"server_group"` + } + err := r.ExtractInto(&s) + return s.ServerGroup, err +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a ServerGroup. +type CreateResult struct { + ServerGroupResult +} + +// GetResult is the response from a Get operation. Call its Extract method to +// interpret it as a ServerGroup. +type GetResult struct { + ServerGroupResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/urls.go new file mode 100644 index 000000000..bca4f320b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups/urls.go @@ -0,0 +1,25 @@ +package servergroups + +import "github.com/gophercloud/gophercloud/v2" + +const resourcePath = "os-server-groups" + +func resourceURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func listURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return resourceURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return getURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/doc.go new file mode 100644 index 000000000..72381f074 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/doc.go @@ -0,0 +1,313 @@ +/* +Package servers provides information and interaction with the server API +resource in the OpenStack Compute service. + +A server is a virtual machine instance in the compute system. In order for +one to be provisioned, a valid flavor and image are required. + +Example to List Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.ListSimple(computeClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to List Detail Servers + + listOpts := servers.ListOpts{ + AllTenants: true, + } + + allPages, err := servers.List(computeClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allServers, err := servers.ExtractServers(allPages) + if err != nil { + panic(err) + } + + for _, server := range allServers { + fmt.Printf("%+v\n", server) + } + +Example to Create a Server + + createOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + server, err := servers.Create(context.TODO(), computeClient, createOpts, nil).Extract() + if err != nil { + panic(err) + } + +Example to Add a Server to a Server Group + + schedulerHintOpts := servers.SchedulerHintOpts{ + Group: "servergroup-uuid", + } + + createOpts := servers.CreateOpts{ + Name: "server_name", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + server, err := servers.Create(context.TODO(), computeClient, createOpts, schedulerHintOpts).Extract() + if err != nil { + panic(err) + } + +Example to Place Server B on a Different Host than Server A + + schedulerHintOpts := servers.SchedulerHintOpts{ + DifferentHost: []string{ + "server-a-uuid", + } + } + + createOpts := servers.CreateOpts{ + Name: "server_b", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + server, err := servers.Create(context.TODO(), computeClient, createOpts, schedulerHintOpts).Extract() + if err != nil { + panic(err) + } + +Example to Place Server B on the Same Host as Server A + + schedulerHintOpts := servers.SchedulerHintOpts{ + SameHost: []string{ + "server-a-uuid", + } + } + + createOpts := servers.CreateOpts{ + Name: "server_b", + ImageRef: "image-uuid", + FlavorRef: "flavor-uuid", + } + + server, err := servers.Create(context.TODO(), computeClient, createOpts, schedulerHintOpts).Extract() + if err != nil { + panic(err) + } + +# Example to Create a Server From an Image + +This example will boot a server from an image and use a standard ephemeral +disk as the server's root disk. This is virtually no different than creating +a server without using block device mappings. + + blockDevices := []servers.BlockDevice{ + servers.BlockDevice{ + BootIndex: 0, + DeleteOnTermination: true, + DestinationType: servers.DestinationLocal, + SourceType: servers.SourceImage, + UUID: "image-uuid", + }, + } + + createOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + BlockDevice: blockDevices, + } + + server, err := servers.Create(context.TODO(), client, createOpts, nil).Extract() + if err != nil { + panic(err) + } + +# Example to Create a Server From a New Volume + +This example will create a block storage volume based on the given Image. The +server will use this volume as its root disk. + + blockDevices := []servers.BlockDevice{ + servers.BlockDevice{ + DeleteOnTermination: true, + DestinationType: servers.DestinationVolume, + SourceType: servers.SourceImage, + UUID: "image-uuid", + VolumeSize: 2, + }, + } + + createOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + BlockDevice: blockDevices, + } + + server, err := servers.Create(context.TODO(), client, createOpts, nil).Extract() + if err != nil { + panic(err) + } + +# Example to Create a Server From an Existing Volume + +This example will create a server with an existing volume as its root disk. + + blockDevices := []servers.BlockDevice{ + servers.BlockDevice{ + DeleteOnTermination: true, + DestinationType: servers.DestinationVolume, + SourceType: servers.SourceVolume, + UUID: "volume-uuid", + }, + } + + createOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + BlockDevice: blockDevices, + } + + server, err := servers.Create(context.TODO(), client, createOpts, nil).Extract() + if err != nil { + panic(err) + } + +# Example to Create a Server with Multiple Ephemeral Disks + +This example will create a server with multiple ephemeral disks. The first +block device will be based off of an existing Image. Each additional +ephemeral disks must have an index of -1. + + blockDevices := []servers.BlockDevice{ + servers.BlockDevice{ + BootIndex: 0, + DestinationType: servers.DestinationLocal, + DeleteOnTermination: true, + SourceType: servers.SourceImage, + UUID: "image-uuid", + VolumeSize: 5, + }, + servers.BlockDevice{ + BootIndex: -1, + DestinationType: servers.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: servers.SourceBlank, + VolumeSize: 1, + }, + servers.BlockDevice{ + BootIndex: -1, + DestinationType: servers.DestinationLocal, + DeleteOnTermination: true, + GuestFormat: "ext4", + SourceType: servers.SourceBlank, + VolumeSize: 1, + }, + } + + CreateOpts := servers.CreateOpts{ + Name: "server_name", + FlavorRef: "flavor-uuid", + ImageRef: "image-uuid", + BlockDevice: blockDevices, + } + + server, err := servers.Create(context.TODO(), client, createOpts, nil).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.Delete(context.TODO(), computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Force Delete a Server + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + err := servers.ForceDelete(context.TODO(), computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Reboot a Server + + rebootOpts := servers.RebootOpts{ + Type: servers.SoftReboot, + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Reboot(context.TODO(), computeClient, serverID, rebootOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Rebuild a Server + + rebuildOpts := servers.RebuildOpts{ + Name: "new_name", + ImageID: "image-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + server, err := servers.Rebuilt(computeClient, serverID, rebuildOpts).Extract() + if err != nil { + panic(err) + } + +Example to Resize a Server + + resizeOpts := servers.ResizeOpts{ + FlavorRef: "flavor-uuid", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + err := servers.Resize(context.TODO(), computeClient, serverID, resizeOpts).ExtractErr() + if err != nil { + panic(err) + } + + err = servers.ConfirmResize(context.TODO(), computeClient, serverID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Snapshot a Server + + snapshotOpts := servers.CreateImageOpts{ + Name: "snapshot_name", + } + + serverID := "d9072956-1560-487c-97f2-18bdf65ec749" + + image, err := servers.CreateImage(context.TODO(), computeClient, serverID, snapshotOpts).ExtractImageID() + if err != nil { + panic(err) + } +*/ +package servers diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/errors.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/errors.go new file mode 100644 index 000000000..a6eda38f4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/errors.go @@ -0,0 +1,71 @@ +package servers + +import ( + "fmt" + + "github.com/gophercloud/gophercloud/v2" +) + +// ErrNeitherImageIDNorImageNameProvided is the error when neither the image +// ID nor the image name is provided for a server operation +type ErrNeitherImageIDNorImageNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherImageIDNorImageNameProvided) Error() string { + return "One and only one of the image ID and the image name must be provided." +} + +// ErrNeitherFlavorIDNorFlavorNameProvided is the error when neither the flavor +// ID nor the flavor name is provided for a server operation +type ErrNeitherFlavorIDNorFlavorNameProvided struct{ gophercloud.ErrMissingInput } + +func (e ErrNeitherFlavorIDNorFlavorNameProvided) Error() string { + return "One and only one of the flavor ID and the flavor name must be provided." +} + +type ErrNoClientProvidedForIDByName struct{ gophercloud.ErrMissingInput } + +func (e ErrNoClientProvidedForIDByName) Error() string { + return "A service client must be provided to find a resource ID by name." +} + +// ErrInvalidHowParameterProvided is the error when an unknown value is given +// for the `how` argument +type ErrInvalidHowParameterProvided struct{ gophercloud.ErrInvalidInput } + +// ErrNoAdminPassProvided is the error when an administrative password isn't +// provided for a server operation +type ErrNoAdminPassProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoImageIDProvided is the error when an image ID isn't provided for a server +// operation +type ErrNoImageIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrNoIDProvided is the error when a server ID isn't provided for a server +// operation +type ErrNoIDProvided struct{ gophercloud.ErrMissingInput } + +// ErrServer is a generic error type for servers HTTP operations. +type ErrServer struct { + gophercloud.ErrUnexpectedResponseCode + ID string +} + +func (se ErrServer) Error() string { + return fmt.Sprintf("Error while executing HTTP request for server [%s]", se.ID) +} + +// Error404 overrides the generic 404 error message. +func (se ErrServer) Error404(e gophercloud.ErrUnexpectedResponseCode) error { + se.ErrUnexpectedResponseCode = e + return &ErrServerNotFound{se} +} + +// ErrServerNotFound is the error when a 404 is received during server HTTP +// operations. +type ErrServerNotFound struct { + ErrServer +} + +func (e ErrServerNotFound) Error() string { + return fmt.Sprintf("I couldn't find server [%s]", e.ID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/requests.go new file mode 100644 index 000000000..c0ccebfa4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/requests.go @@ -0,0 +1,1405 @@ +package servers + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "maps" + "net" + "regexp" + "strings" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToServerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +type ListOpts struct { + // ChangesSince is a time/date stamp for when the server last changed status. + ChangesSince string `q:"changes-since"` + + // Image is the name of the image in URL format. + Image string `q:"image"` + + // Flavor is the name of the flavor in URL format. + Flavor string `q:"flavor"` + + // IP is a regular expression to match the IPv4 address of the server. + IP string `q:"ip"` + + // This requires the client to be set to microversion 2.5 or later, unless + // the user is an admin. + // IP is a regular expression to match the IPv6 address of the server. + IP6 string `q:"ip6"` + + // Name of the server as a string; can be queried with regular expressions. + // Realize that ?name=bob returns both bob and bobb. If you need to match bob + // only, you can use a regular expression matching the syntax of the + // underlying database server implemented for Compute. + Name string `q:"name"` + + // Status is the value of the status of the server so that you can filter on + // "ACTIVE" for example. + Status string `q:"status"` + + // Host is the name of the host as a string. + Host string `q:"host"` + + // Marker is a UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Limit is an integer value for the limit of values to return. + Limit int `q:"limit"` + + // AllTenants is a bool to show all tenants. + AllTenants bool `q:"all_tenants"` + + // TenantID lists servers for a particular tenant. + // Setting "AllTenants = true" is required. + TenantID string `q:"tenant_id"` + + // This requires the client to be set to microversion 2.83 or later, unless + // the user is an admin. + // UserID lists servers for a particular user. + UserID string `q:"user_id"` + + // This requires the client to be set to microversion 2.26 or later. + // Tags filters on specific server tags. All tags must be present for the server. + Tags string `q:"tags"` + + // This requires the client to be set to microversion 2.26 or later. + // TagsAny filters on specific server tags. At least one of the tags must be present for the server. + TagsAny string `q:"tags-any"` + + // This requires the client to be set to microversion 2.26 or later. + // NotTags filters on specific server tags. All tags must be absent for the server. + NotTags string `q:"not-tags"` + + // This requires the client to be set to microversion 2.26 or later. + // NotTagsAny filters on specific server tags. At least one of the tags must be absent for the server. + NotTagsAny string `q:"not-tags-any"` + + // Display servers based on their availability zone (Admin only until microversion 2.82). + AvailabilityZone string `q:"availability_zone"` +} + +// ToServerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToServerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListSimple makes a request against the API to list servers accessible to you. +func ListSimple(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// List makes a request against the API to list servers details accessible to you. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listDetailURL(client) + if opts != nil { + query, err := opts.ToServerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ServerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// SchedulerHintOptsBuilder builds the scheduler hints into a serializable format. +type SchedulerHintOptsBuilder interface { + ToSchedulerHintsMap() (map[string]any, error) +} + +// SchedulerHintOpts represents a set of scheduling hints that are passed to the +// OpenStack scheduler. +type SchedulerHintOpts struct { + // Group specifies a Server Group to place the instance in. + Group string + + // DifferentHost will place the instance on a compute node that does not + // host the given instances. + DifferentHost []string + + // SameHost will place the instance on a compute node that hosts the given + // instances. + SameHost []string + + // Query is a conditional statement that results in compute nodes able to + // host the instance. + Query []any + + // TargetCell specifies a cell name where the instance will be placed. + TargetCell string `json:"target_cell,omitempty"` + + // DifferentCell specifies cells names where an instance should not be placed. + DifferentCell []string `json:"different_cell,omitempty"` + + // BuildNearHostIP specifies a subnet of compute nodes to host the instance. + BuildNearHostIP string + + // AdditionalProperies are arbitrary key/values that are not validated by nova. + AdditionalProperties map[string]any +} + +// ToSchedulerHintsMap assembles a request body for scheduler hints. +func (opts SchedulerHintOpts) ToSchedulerHintsMap() (map[string]any, error) { + sh := make(map[string]any) + + uuidRegex, _ := regexp.Compile("^[a-z0-9]{8}-[a-z0-9]{4}-[1-5][a-z0-9]{3}-[a-z0-9]{4}-[a-z0-9]{12}$") + + if opts.Group != "" { + if !uuidRegex.MatchString(opts.Group) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.schedulerhints.SchedulerHintOpts.Group" + err.Value = opts.Group + err.Info = "Group must be a UUID" + return nil, err + } + sh["group"] = opts.Group + } + + if len(opts.DifferentHost) > 0 { + for _, diffHost := range opts.DifferentHost { + if !uuidRegex.MatchString(diffHost) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.schedulerhints.SchedulerHintOpts.DifferentHost" + err.Value = opts.DifferentHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["different_host"] = opts.DifferentHost + } + + if len(opts.SameHost) > 0 { + for _, sameHost := range opts.SameHost { + if !uuidRegex.MatchString(sameHost) { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.schedulerhints.SchedulerHintOpts.SameHost" + err.Value = opts.SameHost + err.Info = "The hosts must be in UUID format." + return nil, err + } + } + sh["same_host"] = opts.SameHost + } + + /* + Query can be something simple like: + [">=", "$free_ram_mb", 1024] + + Or more complex like: + ['and', + ['>=', '$free_ram_mb', 1024], + ['>=', '$free_disk_mb', 200 * 1024] + ] + + Because of the possible complexity, just make sure the length is a minimum of 3. + */ + if len(opts.Query) > 0 { + if len(opts.Query) < 3 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.schedulerhints.SchedulerHintOpts.Query" + err.Value = opts.Query + err.Info = "Must be a conditional statement in the format of [op,variable,value]" + return nil, err + } + + // The query needs to be sent as a marshalled string. + b, err := json.Marshal(opts.Query) + if err != nil { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.schedulerhints.SchedulerHintOpts.Query" + err.Value = opts.Query + err.Info = "Must be a conditional statement in the format of [op,variable,value]" + return nil, err + } + + sh["query"] = string(b) + } + + if opts.TargetCell != "" { + sh["target_cell"] = opts.TargetCell + } + + if len(opts.DifferentCell) > 0 { + sh["different_cell"] = opts.DifferentCell + } + + if opts.BuildNearHostIP != "" { + if _, _, err := net.ParseCIDR(opts.BuildNearHostIP); err != nil { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.schedulerhints.SchedulerHintOpts.BuildNearHostIP" + err.Value = opts.BuildNearHostIP + err.Info = "Must be a valid subnet in the form 192.168.1.1/24" + return nil, err + } + ipParts := strings.Split(opts.BuildNearHostIP, "/") + sh["build_near_host_ip"] = ipParts[0] + sh["cidr"] = "/" + ipParts[1] + } + + if opts.AdditionalProperties != nil { + for k, v := range opts.AdditionalProperties { + sh[k] = v + } + } + + if len(sh) == 0 { + return sh, nil + } + + return map[string]any{"os:scheduler_hints": sh}, nil +} + +// Network is used within CreateOpts to control a new server's network +// attachments. +type Network struct { + // UUID of a network to attach to the newly provisioned server. + // Required unless Port is provided. + UUID string + + // Port of a neutron network to attach to the newly provisioned server. + // Required unless UUID is provided. + Port string + + // FixedIP specifies a fixed IPv4 address to be used on this network. + FixedIP string + + // Tag may contain an optional device role tag for the server's virtual + // network interface. This can be used to identify network interfaces when + // multiple networks are connected to one server. + // + // Requires microversion 2.32 through 2.36 or 2.42 or later. + Tag string +} + +type ( + // DestinationType represents the type of medium being used as the + // destination of the bootable device. + DestinationType string + + // SourceType represents the type of medium being used as the source of the + // bootable device. + SourceType string +) + +const ( + // DestinationLocal DestinationType is for using an ephemeral disk as the + // destination. + DestinationLocal DestinationType = "local" + + // DestinationVolume DestinationType is for using a volume as the destination. + DestinationVolume DestinationType = "volume" + + // SourceBlank SourceType is for a "blank" or empty source. + SourceBlank SourceType = "blank" + + // SourceImage SourceType is for using images as the source of a block device. + SourceImage SourceType = "image" + + // SourceSnapshot SourceType is for using a volume snapshot as the source of + // a block device. + SourceSnapshot SourceType = "snapshot" + + // SourceVolume SourceType is for using a volume as the source of block + // device. + SourceVolume SourceType = "volume" +) + +// BlockDevice is a structure with options for creating block devices in a +// server. The block device may be created from an image, snapshot, new volume, +// or existing volume. The destination may be a new volume, existing volume +// which will be attached to the instance, ephemeral disk, or boot device. +type BlockDevice struct { + // SourceType must be one of: "volume", "snapshot", "image", or "blank". + SourceType SourceType `json:"source_type" required:"true"` + + // UUID is the unique identifier for the existing volume, snapshot, or + // image (see above). + UUID string `json:"uuid,omitempty"` + + // BootIndex is the boot index. It defaults to 0. + BootIndex int `json:"boot_index"` + + // DeleteOnTermination specifies whether or not to delete the attached volume + // when the server is deleted. Defaults to `false`. + DeleteOnTermination bool `json:"delete_on_termination"` + + // DestinationType is the type that gets created. Possible values are "volume" + // and "local". + DestinationType DestinationType `json:"destination_type,omitempty"` + + // GuestFormat specifies the format of the block device. + // Not specifying this will cause the device to be formatted to the default in Nova + // which is currently vfat. + // https://opendev.org/openstack/nova/src/commit/d0b459423dd81644e8d9382b6c87fabaa4f03ad4/nova/privsep/fs.py#L257 + GuestFormat string `json:"guest_format,omitempty"` + + // VolumeSize is the size of the volume to create (in gigabytes). This can be + // omitted for existing volumes. + VolumeSize int `json:"volume_size,omitempty"` + + // DeviceType specifies the device type of the block devices. + // Examples of this are disk, cdrom, floppy, lun, etc. + DeviceType string `json:"device_type,omitempty"` + + // DiskBus is the bus type of the block devices. + // Examples of this are ide, usb, virtio, scsi, etc. + DiskBus string `json:"disk_bus,omitempty"` + + // VolumeType is the volume type of the block device. + // This requires Compute API microversion 2.67 or later. + VolumeType string `json:"volume_type,omitempty"` + + // Tag is an arbitrary string that can be applied to a block device. + // Information about the device tags can be obtained from the metadata API + // and the config drive, allowing devices to be easily identified. + // This requires Compute API microversion 2.42 or later. + Tag string `json:"tag,omitempty"` +} + +// Personality is an array of files that are injected into the server at launch. +type Personality []*File + +// File is used within CreateOpts and RebuildOpts to inject a file into the +// server at launch. +// File implements the json.Marshaler interface, so when a Create or Rebuild +// operation is requested, json.Marshal will call File's MarshalJSON method. +type File struct { + // Path of the file. + Path string + + // Contents of the file. Maximum content size is 255 bytes. + Contents []byte +} + +// MarshalJSON marshals the escaped file, base64 encoding the contents. +func (f *File) MarshalJSON() ([]byte, error) { + file := struct { + Path string `json:"path"` + Contents string `json:"contents"` + }{ + Path: f.Path, + Contents: base64.StdEncoding.EncodeToString(f.Contents), + } + return json.Marshal(file) +} + +// DiskConfig represents one of the two possible settings for the DiskConfig +// option when creating, rebuilding, or resizing servers: Auto or Manual. +type DiskConfig string + +const ( + // Auto builds a server with a single partition the size of the target flavor + // disk and automatically adjusts the filesystem to fit the entire partition. + // Auto may only be used with images and servers that use a single EXT3 + // partition. + Auto DiskConfig = "AUTO" + + // Manual builds a server using whatever partition scheme and filesystem are + // present in the source image. If the target flavor disk is larger, the + // remaining space is left unpartitioned. This enables images to have non-EXT3 + // filesystems, multiple partitions, and so on, and enables you to manage the + // disk configuration. It also results in slightly shorter boot times. + Manual DiskConfig = "MANUAL" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToServerCreateMap() (map[string]any, error) +} + +// CreateOpts specifies server creation parameters. +type CreateOpts struct { + // Name is the name to assign to the newly launched server. + Name string `json:"name" required:"true"` + + // ImageRef is the ID or full URL to the image that contains the + // server's OS and initial state. + // Also optional if using the boot-from-volume extension. + ImageRef string `json:"imageRef"` + + // FlavorRef is the ID or full URL to the flavor that describes the server's specs. + FlavorRef string `json:"flavorRef"` + + // SecurityGroups lists the names of the security groups to which this server + // should belong. + SecurityGroups []string `json:"-"` + + // UserData contains configuration information or scripts to use upon launch. + // Create will base64-encode it for you, if it isn't already. + UserData []byte `json:"-"` + + // AvailabilityZone in which to launch the server. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // Networks dictates how this server will be attached to available networks. + // By default, the server will be attached to all isolated networks for the + // tenant. + // Starting with microversion 2.37 networks can also be an "auto" or "none" + // string. + Networks any `json:"-"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to the + // server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality includes files to inject into the server at launch. + // Create will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive *bool `json:"config_drive,omitempty"` + + // AdminPass sets the root user password. If not set, a randomly-generated + // password will be created and returned in the response. + AdminPass string `json:"adminPass,omitempty"` + + // AccessIPv4 specifies an IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 specifies an IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Min specifies Minimum number of servers to launch. + Min int `json:"min_count,omitempty"` + + // Max specifies Maximum number of servers to launch. + Max int `json:"max_count,omitempty"` + + // Tags allows a server to be tagged with single-word metadata. + // Requires microversion 2.52 or later. + Tags []string `json:"tags,omitempty"` + + // (Available from 2.90) Hostname specifies the hostname to configure for the + // instance in the metadata service. Starting with microversion 2.94, this can + // be a Fully Qualified Domain Name (FQDN) of up to 255 characters in length. + // If not set, OpenStack will derive the server's hostname from the Name field. + Hostname string `json:"hostname,omitempty"` + + // BlockDevice describes the mapping of various block devices. + BlockDevice []BlockDevice `json:"block_device_mapping_v2,omitempty"` + + // DiskConfig [optional] controls how the created server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` + + // HypervisorHostname is the name of the hypervisor to which the server is scheduled. + HypervisorHostname string `json:"hypervisor_hostname,omitempty"` +} + +// ToServerCreateMap assembles a request body based on the contents of a +// CreateOpts. +func (opts CreateOpts) ToServerCreateMap() (map[string]any, error) { + // We intentionally don't envelope the body here since we want to strip + // some fields out and modify others + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.UserData != nil { + var userData string + if _, err := base64.StdEncoding.DecodeString(string(opts.UserData)); err != nil { + userData = base64.StdEncoding.EncodeToString(opts.UserData) + } else { + userData = string(opts.UserData) + } + b["user_data"] = &userData + } + + if len(opts.SecurityGroups) > 0 { + securityGroups := make([]map[string]any, len(opts.SecurityGroups)) + for i, groupName := range opts.SecurityGroups { + securityGroups[i] = map[string]any{"name": groupName} + } + b["security_groups"] = securityGroups + } + + switch v := opts.Networks.(type) { + case []Network: + if len(v) > 0 { + networks := make([]map[string]any, len(v)) + for i, net := range v { + networks[i] = make(map[string]any) + if net.UUID != "" { + networks[i]["uuid"] = net.UUID + } + if net.Port != "" { + networks[i]["port"] = net.Port + } + if net.FixedIP != "" { + networks[i]["fixed_ip"] = net.FixedIP + } + if net.Tag != "" { + networks[i]["tag"] = net.Tag + } + } + b["networks"] = networks + } + case string: + if v == "auto" || v == "none" { + b["networks"] = v + } else { + return nil, fmt.Errorf(`networks must be a slice of Network struct or a string with "auto" or "none" values, current value is %q`, v) + } + } + + if opts.Min != 0 { + b["min_count"] = opts.Min + } + + if opts.Max != 0 { + b["max_count"] = opts.Max + } + + // Now we do our enveloping + b = map[string]any{"server": b} + + return b, nil +} + +// Create requests a server to be provisioned to the user in the current tenant. +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder, hintOpts SchedulerHintOptsBuilder) (r CreateResult) { + b, err := opts.ToServerCreateMap() + if err != nil { + r.Err = err + return + } + + if hintOpts != nil { + sh, err := hintOpts.ToSchedulerHintsMap() + if err != nil { + r.Err = err + return + } + maps.Copy(b, sh) + } + + resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete requests that a server previously provisioned be removed from your +// account. +func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(ctx, deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ForceDelete forces the deletion of a server. +func ForceDelete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"forceDelete": ""}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get requests details on a single server, by ID. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, getURL(client, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional attributes to the +// Update request. +type UpdateOptsBuilder interface { + ToServerUpdateMap() (map[string]any, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// server. +type UpdateOpts struct { + // Name changes the displayed name of the server. + // The server host name will *not* change. + // Server names are not constrained to be unique, even within the same tenant. + Name string `json:"name,omitempty"` + + // AccessIPv4 provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Hostname changes the hostname of the server. + // Requires microversion 2.90 or later. + // Note: This information is published via the metadata service and requires + // application such as cloud-init to propagate it through to the instance. + Hostname *string `json:"hostname,omitempty"` +} + +// ToServerUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToServerUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "server") +} + +// Update requests that various attributes of the indicated server be changed. +func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToServerUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ChangeAdminPassword alters the administrator or root password for a specified +// server. +func ChangeAdminPassword(ctx context.Context, client *gophercloud.ServiceClient, id, newPassword string) (r ActionResult) { + b := map[string]any{ + "changePassword": map[string]string{ + "adminPass": newPassword, + }, + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RebootMethod describes the mechanisms by which a server reboot can be requested. +type RebootMethod string + +// These constants determine how a server should be rebooted. +// See the Reboot() function for further details. +const ( + SoftReboot RebootMethod = "SOFT" + HardReboot RebootMethod = "HARD" + OSReboot = SoftReboot + PowerCycle = HardReboot +) + +// RebootOptsBuilder allows extensions to add additional parameters to the +// reboot request. +type RebootOptsBuilder interface { + ToServerRebootMap() (map[string]any, error) +} + +// RebootOpts provides options to the reboot request. +type RebootOpts struct { + // Type is the type of reboot to perform on the server. + Type RebootMethod `json:"type" required:"true"` +} + +// ToServerRebootMap builds a body for the reboot request. +func (opts RebootOpts) ToServerRebootMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "reboot") +} + +/* +Reboot requests that a given server reboot. + +Two methods exist for rebooting a server: + +HardReboot (aka PowerCycle) starts the server instance by physically cutting +power to the machine, or if a VM, terminating it at the hypervisor level. +It's done. Caput. Full stop. +Then, after a brief while, power is restored or the VM instance restarted. + +SoftReboot (aka OSReboot) simply tells the OS to restart under its own +procedure. +E.g., in Linux, asking it to enter runlevel 6, or executing +"sudo shutdown -r now", or by asking Windows to rtart the machine. +*/ +func Reboot(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RebootOptsBuilder) (r ActionResult) { + b, err := opts.ToServerRebootMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RebuildOptsBuilder allows extensions to provide additional parameters to the +// rebuild request. +type RebuildOptsBuilder interface { + ToServerRebuildMap() (map[string]any, error) +} + +// RebuildOpts represents the configuration options used in a server rebuild +// operation. +type RebuildOpts struct { + // AdminPass is the server's admin password + AdminPass string `json:"adminPass,omitempty"` + + // ImageRef is the ID of the image you want your server to be provisioned on. + ImageRef string `json:"imageRef"` + + // Name to set the server to + Name string `json:"name,omitempty"` + + // AccessIPv4 [optional] provides a new IPv4 address for the instance. + AccessIPv4 string `json:"accessIPv4,omitempty"` + + // AccessIPv6 [optional] provides a new IPv6 address for the instance. + AccessIPv6 string `json:"accessIPv6,omitempty"` + + // Metadata [optional] contains key-value pairs (up to 255 bytes each) + // to attach to the server. + Metadata map[string]string `json:"metadata,omitempty"` + + // Personality [optional] includes files to inject into the server at launch. + // Rebuild will base64-encode file contents for you. + Personality Personality `json:"personality,omitempty"` + + // DiskConfig controls how the rebuilt server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerRebuildMap formats a RebuildOpts struct into a map for use in JSON +func (opts RebuildOpts) ToServerRebuildMap() (map[string]any, error) { + if opts.DiskConfig != "" && opts.DiskConfig != Auto && opts.DiskConfig != Manual { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.RebuildOpts.DiskConfig" + err.Info = "Must be either diskconfig.Auto or diskconfig.Manual" + return nil, err + } + + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + return map[string]any{"rebuild": b}, nil +} + +// Rebuild will reprovision the server according to the configuration options +// provided in the RebuildOpts struct. +func Rebuild(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RebuildOptsBuilder) (r RebuildResult) { + b, err := opts.ToServerRebuildMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResizeOptsBuilder allows extensions to add additional parameters to the +// resize request. +type ResizeOptsBuilder interface { + ToServerResizeMap() (map[string]any, error) +} + +// ResizeOpts represents the configuration options used to control a Resize +// operation. +type ResizeOpts struct { + // FlavorRef is the ID of the flavor you wish your server to become. + FlavorRef string `json:"flavorRef" required:"true"` + + // DiskConfig [optional] controls how the resized server's disk is partitioned. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig,omitempty"` +} + +// ToServerResizeMap formats a ResizeOpts as a map that can be used as a JSON +// request body for the Resize request. +func (opts ResizeOpts) ToServerResizeMap() (map[string]any, error) { + if opts.DiskConfig != "" && opts.DiskConfig != Auto && opts.DiskConfig != Manual { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.ResizeOpts.DiskConfig" + err.Info = "Must be either diskconfig.Auto or diskconfig.Manual" + return nil, err + } + + return gophercloud.BuildRequestBody(opts, "resize") +} + +// Resize instructs the provider to change the flavor of the server. +// +// Note that this implies rebuilding it. +// +// Unfortunately, one cannot pass rebuild parameters to the resize function. +// When the resize completes, the server will be in VERIFY_RESIZE state. +// While in this state, you can explore the use of the new server's +// configuration. If you like it, call ConfirmResize() to commit the resize +// permanently. Otherwise, call RevertResize() to restore the old configuration. +func Resize(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ResizeOptsBuilder) (r ActionResult) { + b, err := opts.ToServerResizeMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ConfirmResize confirms a previous resize operation on a server. +// See Resize() for more details. +func ConfirmResize(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"confirmResize": nil}, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201, 202, 204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RevertResize cancels a previous resize operation on a server. +// See Resize() for more details. +func RevertResize(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ActionResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"revertResize": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResetMetadataOptsBuilder allows extensions to add additional parameters to +// the Reset request. +type ResetMetadataOptsBuilder interface { + ToMetadataResetMap() (map[string]any, error) +} + +// MetadataOpts is a map that contains key-value pairs. +type MetadataOpts map[string]string + +// ToMetadataResetMap assembles a body for a Reset request based on the contents +// of a MetadataOpts. +func (opts MetadataOpts) ToMetadataResetMap() (map[string]any, error) { + return map[string]any{"metadata": opts}, nil +} + +// ToMetadataUpdateMap assembles a body for an Update request based on the +// contents of a MetadataOpts. +func (opts MetadataOpts) ToMetadataUpdateMap() (map[string]any, error) { + return map[string]any{"metadata": opts}, nil +} + +// ResetMetadata will create multiple new key-value pairs for the given server +// ID. +// Note: Using this operation will erase any already-existing metadata and +// create the new metadata provided. To keep any already-existing metadata, +// use the UpdateMetadatas or UpdateMetadata function. +func ResetMetadata(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ResetMetadataOptsBuilder) (r ResetMetadataResult) { + b, err := opts.ToMetadataResetMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Metadata requests all the metadata for the given server ID. +func Metadata(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetMetadataResult) { + resp, err := client.Get(ctx, metadataURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateMetadataOptsBuilder allows extensions to add additional parameters to +// the Create request. +type UpdateMetadataOptsBuilder interface { + ToMetadataUpdateMap() (map[string]any, error) +} + +// UpdateMetadata updates (or creates) all the metadata specified by opts for +// the given server ID. This operation does not affect already-existing metadata +// that is not specified by opts. +func UpdateMetadata(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateMetadataOptsBuilder) (r UpdateMetadataResult) { + b, err := opts.ToMetadataUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, metadataURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// MetadatumOptsBuilder allows extensions to add additional parameters to the +// Create request. +type MetadatumOptsBuilder interface { + ToMetadatumCreateMap() (map[string]any, string, error) +} + +// MetadatumOpts is a map of length one that contains a key-value pair. +type MetadatumOpts map[string]string + +// ToMetadatumCreateMap assembles a body for a Create request based on the +// contents of a MetadataumOpts. +func (opts MetadatumOpts) ToMetadatumCreateMap() (map[string]any, string, error) { + if len(opts) != 1 { + err := gophercloud.ErrInvalidInput{} + err.Argument = "servers.MetadatumOpts" + err.Info = "Must have 1 and only 1 key-value pair" + return nil, "", err + } + metadatum := map[string]any{"meta": opts} + var key string + for k := range metadatum["meta"].(MetadatumOpts) { + key = k + } + return metadatum, key, nil +} + +// CreateMetadatum will create or update the key-value pair with the given key +// for the given server ID. +func CreateMetadatum(ctx context.Context, client *gophercloud.ServiceClient, id string, opts MetadatumOptsBuilder) (r CreateMetadatumResult) { + b, key, err := opts.ToMetadatumCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, metadatumURL(client, id, key), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Metadatum requests the key-value pair with the given key for the given +// server ID. +func Metadatum(ctx context.Context, client *gophercloud.ServiceClient, id, key string) (r GetMetadatumResult) { + resp, err := client.Get(ctx, metadatumURL(client, id, key), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteMetadatum will delete the key-value pair with the given key for the +// given server ID. +func DeleteMetadatum(ctx context.Context, client *gophercloud.ServiceClient, id, key string) (r DeleteMetadatumResult) { + resp, err := client.Delete(ctx, metadatumURL(client, id, key), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAddresses makes a request against the API to list the servers IP +// addresses. +func ListAddresses(client *gophercloud.ServiceClient, id string) pagination.Pager { + return pagination.NewPager(client, listAddressesURL(client, id), func(r pagination.PageResult) pagination.Page { + return AddressPage{pagination.SinglePageBase(r)} + }) +} + +// ListAddressesByNetwork makes a request against the API to list the servers IP +// addresses for the given network. +func ListAddressesByNetwork(client *gophercloud.ServiceClient, id, network string) pagination.Pager { + return pagination.NewPager(client, listAddressesByNetworkURL(client, id, network), func(r pagination.PageResult) pagination.Page { + return NetworkAddressPage{pagination.SinglePageBase(r)} + }) +} + +// CreateImageOptsBuilder allows extensions to add additional parameters to the +// CreateImage request. +type CreateImageOptsBuilder interface { + ToServerCreateImageMap() (map[string]any, error) +} + +// CreateImageOpts provides options to pass to the CreateImage request. +type CreateImageOpts struct { + // Name of the image/snapshot. + Name string `json:"name" required:"true"` + + // Metadata contains key-value pairs (up to 255 bytes each) to attach to + // the created image. + Metadata map[string]string `json:"metadata,omitempty"` +} + +// ToServerCreateImageMap formats a CreateImageOpts structure into a request +// body. +func (opts CreateImageOpts) ToServerCreateImageMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "createImage") +} + +// CreateImage makes a request against the nova API to schedule an image to be +// created of the server +func CreateImage(ctx context.Context, client *gophercloud.ServiceClient, id string, opts CreateImageOptsBuilder) (r CreateImageResult) { + b, err := opts.ToServerCreateImageMap() + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(ctx, actionURL(client, id), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + KeepResponseBody: true, + }) + + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + if r.Err != nil { + return + } + defer resp.Body.Close() + + if v := r.Header.Get("Content-Type"); v != "application/json" { + return + } + + // The response body is expected to be a small JSON object containing only "image_id". + // Read it fully into memory so the response body can be closed immediately. + // If the caller doesn't read from the buffer, it can still be safely garbage collected. + + var buf bytes.Buffer + + _, r.Err = io.Copy(&buf, resp.Body) + if r.Err != nil { + return + } + + r.Body = &buf + + return +} + +// GetPassword makes a request against the nova API to get the encrypted +// administrative password. +func GetPassword(ctx context.Context, client *gophercloud.ServiceClient, serverId string) (r GetPasswordResult) { + resp, err := client.Get(ctx, passwordURL(client, serverId), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ShowConsoleOutputOptsBuilder is the interface types must satisfy in order to be +// used as ShowConsoleOutput options +type ShowConsoleOutputOptsBuilder interface { + ToServerShowConsoleOutputMap() (map[string]any, error) +} + +// ShowConsoleOutputOpts satisfies the ShowConsoleOutputOptsBuilder +type ShowConsoleOutputOpts struct { + // The number of lines to fetch from the end of console log. + // All lines will be returned if this is not specified. + Length int `json:"length,omitempty"` +} + +// ToServerShowConsoleOutputMap formats a ShowConsoleOutputOpts structure into a request body. +func (opts ShowConsoleOutputOpts) ToServerShowConsoleOutputMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-getConsoleOutput") +} + +// ShowConsoleOutput makes a request against the nova API to get console log from the server +func ShowConsoleOutput(ctx context.Context, client *gophercloud.ServiceClient, id string, opts ShowConsoleOutputOptsBuilder) (r ShowConsoleOutputResult) { + b, err := opts.ToServerShowConsoleOutputMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// EvacuateOptsBuilder allows extensions to add additional parameters to the +// the Evacuate request. +type EvacuateOptsBuilder interface { + ToEvacuateMap() (map[string]any, error) +} + +// EvacuateOpts specifies Evacuate action parameters. +type EvacuateOpts struct { + // The name of the host to which the server is evacuated + Host string `json:"host,omitempty"` + + // Indicates whether server is on shared storage + OnSharedStorage bool `json:"onSharedStorage"` + + // An administrative password to access the evacuated server + AdminPass string `json:"adminPass,omitempty"` +} + +// ToServerGroupCreateMap constructs a request body from CreateOpts. +func (opts EvacuateOpts) ToEvacuateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "evacuate") +} + +// Evacuate will Evacuate a failed instance to another host. +func Evacuate(ctx context.Context, client *gophercloud.ServiceClient, id string, opts EvacuateOptsBuilder) (r EvacuateResult) { + b, err := opts.ToEvacuateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// InjectNetworkInfo will inject the network info into a server +func InjectNetworkInfo(ctx context.Context, client *gophercloud.ServiceClient, id string) (r InjectNetworkResult) { + b := map[string]any{ + "injectNetworkInfo": nil, + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Lock is the operation responsible for locking a Compute server. +func Lock(ctx context.Context, client *gophercloud.ServiceClient, id string) (r LockResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"lock": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Unlock is the operation responsible for unlocking a Compute server. +func Unlock(ctx context.Context, client *gophercloud.ServiceClient, id string) (r UnlockResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"unlock": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Migrate will initiate a migration of the instance to another host. +func Migrate(ctx context.Context, client *gophercloud.ServiceClient, id string) (r MigrateResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"migrate": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// LiveMigrateOptsBuilder allows extensions to add additional parameters to the +// LiveMigrate request. +type LiveMigrateOptsBuilder interface { + ToLiveMigrateMap() (map[string]any, error) +} + +// LiveMigrateOpts specifies parameters of live migrate action. +type LiveMigrateOpts struct { + // The host to which to migrate the server. + // If this parameter is None, the scheduler chooses a host. + Host *string `json:"host"` + + // Set to True to migrate local disks by using block migration. + // If the source or destination host uses shared storage and you set + // this value to True, the live migration fails. + BlockMigration *bool `json:"block_migration,omitempty"` + + // Set to True to enable over commit when the destination host is checked + // for available disk space. Set to False to disable over commit. This setting + // affects only the libvirt virt driver. + DiskOverCommit *bool `json:"disk_over_commit,omitempty"` +} + +// ToLiveMigrateMap constructs a request body from LiveMigrateOpts. +func (opts LiveMigrateOpts) ToLiveMigrateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "os-migrateLive") +} + +// LiveMigrate will initiate a live-migration (without rebooting) of the instance to another host. +func LiveMigrate(ctx context.Context, client *gophercloud.ServiceClient, id string, opts LiveMigrateOptsBuilder) (r MigrateResult) { + b, err := opts.ToLiveMigrateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Pause is the operation responsible for pausing a Compute server. +func Pause(ctx context.Context, client *gophercloud.ServiceClient, id string) (r PauseResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"pause": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Unpause is the operation responsible for unpausing a Compute server. +func Unpause(ctx context.Context, client *gophercloud.ServiceClient, id string) (r UnpauseResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"unpause": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RescueOptsBuilder is an interface that allows extensions to override the +// default structure of a Rescue request. +type RescueOptsBuilder interface { + ToServerRescueMap() (map[string]any, error) +} + +// RescueOpts represents the configuration options used to control a Rescue +// option. +type RescueOpts struct { + // AdminPass is the desired administrative password for the instance in + // RESCUE mode. + // If it's left blank, the server will generate a password. + AdminPass string `json:"adminPass,omitempty"` + + // RescueImageRef contains reference on an image that needs to be used as + // rescue image. + // If it's left blank, the server will be rescued with the default image. + RescueImageRef string `json:"rescue_image_ref,omitempty"` +} + +// ToServerRescueMap formats a RescueOpts as a map that can be used as a JSON +// request body for the Rescue request. +func (opts RescueOpts) ToServerRescueMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "rescue") +} + +// Rescue instructs the provider to place the server into RESCUE mode. +func Rescue(ctx context.Context, client *gophercloud.ServiceClient, id string, opts RescueOptsBuilder) (r RescueResult) { + b, err := opts.ToServerRescueMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Unrescue instructs the provider to return the server from RESCUE mode. +func Unrescue(ctx context.Context, client *gophercloud.ServiceClient, id string) (r UnrescueResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"unrescue": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ResetNetwork will reset the network of a server +func ResetNetwork(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ResetNetworkResult) { + b := map[string]any{ + "resetNetwork": nil, + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ServerState refers to the states usable in ResetState Action +type ServerState string + +const ( + // StateActive returns the state of the server as active + StateActive ServerState = "active" + + // StateError returns the state of the server as error + StateError ServerState = "error" +) + +// ResetState will reset the state of a server +func ResetState(ctx context.Context, client *gophercloud.ServiceClient, id string, state ServerState) (r ResetStateResult) { + stateMap := map[string]any{"state": state} + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"os-resetState": stateMap}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Shelve is the operation responsible for shelving a Compute server. +func Shelve(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ShelveResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"shelve": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ShelveOffload is the operation responsible for Shelve-Offload a Compute server. +func ShelveOffload(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ShelveOffloadResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"shelveOffload": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UnshelveOptsBuilder allows extensions to add additional parameters to the +// Unshelve request. +type UnshelveOptsBuilder interface { + ToUnshelveMap() (map[string]any, error) +} + +// UnshelveOpts specifies parameters of shelve-offload action. +type UnshelveOpts struct { + // Sets the availability zone to unshelve a server + // Available only after nova 2.77 + AvailabilityZone string `json:"availability_zone,omitempty"` +} + +func (opts UnshelveOpts) ToUnshelveMap() (map[string]any, error) { + // Key 'availabilty_zone' is required if the unshelve action is an object + // i.e {"unshelve": {}} will be rejected + b, err := gophercloud.BuildRequestBody(opts, "unshelve") + if err != nil { + return nil, err + } + + if _, ok := b["unshelve"].(map[string]any)["availability_zone"]; !ok { + b["unshelve"] = nil + } + + return b, err +} + +// Unshelve is the operation responsible for unshelve a Compute server. +func Unshelve(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UnshelveOptsBuilder) (r UnshelveResult) { + b, err := opts.ToUnshelveMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, actionURL(client, id), b, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Start is the operation responsible for starting a Compute server. +func Start(ctx context.Context, client *gophercloud.ServiceClient, id string) (r StartResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"os-start": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Stop is the operation responsible for stopping a Compute server. +func Stop(ctx context.Context, client *gophercloud.ServiceClient, id string) (r StopResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"os-stop": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Suspend is the operation responsible for suspending a Compute server. +func Suspend(ctx context.Context, client *gophercloud.ServiceClient, id string) (r SuspendResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"suspend": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Resume is the operation responsible for resuming a Compute server. +func Resume(ctx context.Context, client *gophercloud.ServiceClient, id string) (r ResumeResult) { + resp, err := client.Post(ctx, actionURL(client, id), map[string]any{"resume": nil}, nil, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/results.go new file mode 100644 index 000000000..edc2740f6 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/results.go @@ -0,0 +1,731 @@ +package servers + +import ( + "crypto/rsa" + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "path" + "strconv" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/utils" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type serverResult struct { + gophercloud.Result +} + +// Extract interprets any serverResult as a Server, if possible. +func (r serverResult) Extract() (*Server, error) { + var s Server + err := r.ExtractInto(&s) + return &s, err +} + +func (r serverResult) ExtractInto(v any) error { + return r.Result.ExtractIntoStructPtr(v, "server") +} + +func ExtractServersInto(r pagination.Page, v any) error { + return r.(ServerPage).Result.ExtractIntoSlicePtr(v, "servers") +} + +// CreateResult is the response from a Create operation. Call its Extract +// method to interpret it as a Server. +type CreateResult struct { + serverResult +} + +// GetResult is the response from a Get operation. Call its Extract +// method to interpret it as a Server. +type GetResult struct { + serverResult +} + +// UpdateResult is the response from an Update operation. Call its Extract +// method to interpret it as a Server. +type UpdateResult struct { + serverResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// RebuildResult is the response from a Rebuild operation. Call its Extract +// method to interpret it as a Server. +type RebuildResult struct { + serverResult +} + +// ActionResult represents the result of server action operations, like reboot. +// Call its ExtractErr method to determine if the action succeeded or failed. +type ActionResult struct { + gophercloud.ErrResult +} + +// CreateImageResult is the response from a CreateImage operation. Call its +// ExtractImageID method to retrieve the ID of the newly created image. +type CreateImageResult struct { + gophercloud.Result +} + +// ShowConsoleOutputResult represents the result of console output from a server +type ShowConsoleOutputResult struct { + gophercloud.Result +} + +// Extract will return the console output from a ShowConsoleOutput request. +func (r ShowConsoleOutputResult) Extract() (string, error) { + var s struct { + Output string `json:"output"` + } + + err := r.ExtractInto(&s) + return s.Output, err +} + +// GetPasswordResult represent the result of a get os-server-password operation. +// Call its ExtractPassword method to retrieve the password. +type GetPasswordResult struct { + gophercloud.Result +} + +// ExtractPassword gets the encrypted password. +// If privateKey != nil the password is decrypted with the private key. +// If privateKey == nil the encrypted password is returned and can be decrypted +// with: +// +// echo '' | base64 -D | openssl rsautl -decrypt -inkey +func (r GetPasswordResult) ExtractPassword(privateKey *rsa.PrivateKey) (string, error) { + var s struct { + Password string `json:"password"` + } + err := r.ExtractInto(&s) + if err == nil && privateKey != nil && s.Password != "" { + return decryptPassword(s.Password, privateKey) + } + return s.Password, err +} + +func decryptPassword(encryptedPassword string, privateKey *rsa.PrivateKey) (string, error) { + b64EncryptedPassword := make([]byte, base64.StdEncoding.DecodedLen(len(encryptedPassword))) + + n, err := base64.StdEncoding.Decode(b64EncryptedPassword, []byte(encryptedPassword)) + if err != nil { + return "", fmt.Errorf("Failed to base64 decode encrypted password: %s", err) + } + password, err := rsa.DecryptPKCS1v15(nil, privateKey, b64EncryptedPassword[0:n]) + if err != nil { + return "", fmt.Errorf("Failed to decrypt password: %s", err) + } + + return string(password), nil +} + +// ExtractImageID gets the ID of the newly created server image from the header. +func (r CreateImageResult) ExtractImageID() (string, error) { + if r.Err != nil { + return "", r.Err + } + + microversion := r.Header.Get("X-OpenStack-Nova-API-Version") + + major, minor, err := utils.ParseMicroversion(microversion) + if err != nil { + return "", fmt.Errorf("failed to parse X-OpenStack-Nova-API-Version header: %s", err) + } + + // In microversions prior to 2.45, the image ID was provided in the Location header. + if major < 2 || (major == 2 && minor < 45) { + return r.extractImageIDFromLocationHeader() + } + + // Starting from 2.45, it is included in the response body. + return r.extractImageIDFromResponseBody() +} + +func (r CreateImageResult) extractImageIDFromLocationHeader() (string, error) { + u, err := url.ParseRequestURI(r.Header.Get("Location")) + if err != nil { + return "", err + } + + imageID := path.Base(u.Path) + if imageID == "." || imageID == "/" { + return "", fmt.Errorf("Failed to parse the ID of newly created image: %s", u) + } + + return imageID, nil +} + +func (r CreateImageResult) extractImageIDFromResponseBody() (string, error) { + var response struct { + ImageID string `json:"image_id"` + } + + if err := r.ExtractInto(&response); err != nil { + return "", err + } + + return response.ImageID, nil +} + +// Server represents a server/instance in the OpenStack cloud. +type Server struct { + // ID uniquely identifies this server amongst all other servers, + // including those not accessible to the current tenant. + ID string `json:"id"` + + // TenantID identifies the tenant owning this server resource. + TenantID string `json:"tenant_id"` + + // UserID uniquely identifies the user account owning the tenant. + UserID string `json:"user_id"` + + // Name contains the human-readable name for the server. + Name string `json:"name"` + + // Updated and Created contain ISO-8601 timestamps of when the state of the + // server last changed, and when it was created. + Updated time.Time `json:"updated"` + Created time.Time `json:"created"` + + // HostID is the host where the server is located in the cloud. + HostID string `json:"hostid"` + + // Status contains the current operational status of the server, + // such as IN_PROGRESS or ACTIVE. + Status string `json:"status"` + + // Progress ranges from 0..100. + // A request made against the server completes only once Progress reaches 100. + Progress int `json:"progress"` + + // AccessIPv4 and AccessIPv6 contain the IP addresses of the server, + // suitable for remote access for administration. + AccessIPv4 string `json:"accessIPv4"` + AccessIPv6 string `json:"accessIPv6"` + + // Image refers to a JSON object, which itself indicates the OS image used to + // deploy the server. + Image map[string]any `json:"-"` + + // Flavor refers to a JSON object, which itself indicates the hardware + // configuration of the deployed server. + Flavor map[string]any `json:"flavor"` + + // Addresses includes a list of all IP addresses assigned to the server, + // keyed by pool. + Addresses map[string]any `json:"addresses"` + + // Metadata includes a list of all user-specified key-value pairs attached + // to the server. + Metadata map[string]string `json:"metadata"` + + // Links includes HTTP references to the itself, useful for passing along to + // other APIs that might want a server reference. + Links []any `json:"links"` + + // KeyName indicates which public key was injected into the server on launch. + KeyName string `json:"key_name"` + + // AdminPass will generally be empty (""). However, it will contain the + // administrative password chosen when provisioning a new server without a + // set AdminPass setting in the first place. + // Note that this is the ONLY time this field will be valid. + AdminPass string `json:"adminPass"` + + // SecurityGroups includes the security groups that this instance has applied + // to it. + SecurityGroups []map[string]any `json:"security_groups"` + + // AttachedVolumes includes the volume attachments of this instance + AttachedVolumes []AttachedVolume `json:"os-extended-volumes:volumes_attached"` + + // Fault contains failure information about a server. + Fault Fault `json:"fault"` + + // Tags is a slice/list of string tags in a server. + // The requires microversion 2.26 or later. + Tags *[]string `json:"tags"` + + // ServerGroups is a slice of strings containing the UUIDs of the + // server groups to which the server belongs. Currently this can + // contain at most one entry. + // New in microversion 2.71 + ServerGroups *[]string `json:"server_groups"` + + // Host is the host/hypervisor that the instance is hosted on. + Host string `json:"OS-EXT-SRV-ATTR:host"` + + // InstanceName is the name of the instance. + InstanceName string `json:"OS-EXT-SRV-ATTR:instance_name"` + + // HypervisorHostname is the hostname of the host/hypervisor that the + // instance is hosted on. + HypervisorHostname string `json:"OS-EXT-SRV-ATTR:hypervisor_hostname"` + + // ReservationID is the reservation ID of the instance. + // This requires microversion 2.3 or later. + ReservationID *string `json:"OS-EXT-SRV-ATTR:reservation_id"` + + // LaunchIndex is the launch index of the instance. + // This requires microversion 2.3 or later. + LaunchIndex *int `json:"OS-EXT-SRV-ATTR:launch_index"` + + // RAMDiskID is the ID of the RAM disk image of the instance. + // This requires microversion 2.3 or later. + RAMDiskID *string `json:"OS-EXT-SRV-ATTR:ramdisk_id"` + + // KernelID is the ID of the kernel image of the instance. + // This requires microversion 2.3 or later. + KernelID *string `json:"OS-EXT-SRV-ATTR:kernel_id"` + + // Hostname is the hostname of the instance. + // This requires microversion 2.3 or later. + Hostname *string `json:"OS-EXT-SRV-ATTR:hostname"` + + // RootDeviceName is the name of the root device of the instance. + // This requires microversion 2.3 or later. + RootDeviceName *string `json:"OS-EXT-SRV-ATTR:root_device_name"` + + // Userdata is the userdata of the instance. + // This requires microversion 2.3 or later. + Userdata *string `json:"OS-EXT-SRV-ATTR:user_data"` + + TaskState string `json:"OS-EXT-STS:task_state"` + VmState string `json:"OS-EXT-STS:vm_state"` + PowerState PowerState `json:"OS-EXT-STS:power_state"` + + LaunchedAt time.Time `json:"-"` + TerminatedAt time.Time `json:"-"` + + // DiskConfig is the disk configuration of the server. + DiskConfig DiskConfig `json:"OS-DCF:diskConfig"` + + // AvailabilityZone is the availabilty zone the server is in. + AvailabilityZone string `json:"OS-EXT-AZ:availability_zone"` + + // Locked indicates the lock status of the server + // This requires microversion 2.9 or later + Locked *bool `json:"locked"` + + // ConfigDrive enables metadata injection through a configuration drive. + ConfigDrive bool `json:"-"` +} + +type AttachedVolume struct { + ID string `json:"id"` +} + +type Fault struct { + Code int `json:"code"` + Created time.Time `json:"created"` + Details string `json:"details"` + Message string `json:"message"` +} + +type PowerState int + +type ServerExtendedStatusExt struct { + TaskState string `json:"OS-EXT-STS:task_state"` + VmState string `json:"OS-EXT-STS:vm_state"` + PowerState PowerState `json:"OS-EXT-STS:power_state"` +} + +const ( + NOSTATE = iota + RUNNING + _UNUSED1 + PAUSED + SHUTDOWN + _UNUSED2 + CRASHED + SUSPENDED +) + +func (r PowerState) String() string { + switch r { + case NOSTATE: + return "NOSTATE" + case RUNNING: + return "RUNNING" + case PAUSED: + return "PAUSED" + case SHUTDOWN: + return "SHUTDOWN" + case CRASHED: + return "CRASHED" + case SUSPENDED: + return "SUSPENDED" + case _UNUSED1, _UNUSED2: + return "_UNUSED" + default: + return "N/A" + } +} + +func (r *Server) UnmarshalJSON(b []byte) error { + type tmp Server + var s struct { + tmp + Image any `json:"image"` + LaunchedAt gophercloud.JSONRFC3339MilliNoZ `json:"OS-SRV-USG:launched_at"` + TerminatedAt gophercloud.JSONRFC3339MilliNoZ `json:"OS-SRV-USG:terminated_at"` + ConfigDrive any `json:"config_drive"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Server(s.tmp) + + switch t := s.Image.(type) { + case map[string]any: + r.Image = t + case string: + switch t { + case "": + r.Image = nil + } + } + + r.LaunchedAt = time.Time(s.LaunchedAt) + r.TerminatedAt = time.Time(s.TerminatedAt) + + switch t := s.ConfigDrive.(type) { + case nil: + r.ConfigDrive = false + case bool: + r.ConfigDrive = t + case string: + if t == "" { + r.ConfigDrive = false + } else { + r.ConfigDrive, err = strconv.ParseBool(t) + if err != nil { + return fmt.Errorf("failed to parse ConfigDrive %q: %v", t, err) + } + } + default: + return fmt.Errorf("unknown type for ConfigDrive: %T (value: %v)", t, t) + } + + return err +} + +// ServerPage abstracts the raw results of making a List() request against +// the API. As OpenStack extensions may freely alter the response bodies of +// structures returned to the client, you may only safely access the data +// provided through the ExtractServers call. +type ServerPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if a page contains no Server results. +func (r ServerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + s, err := ExtractServers(r) + return len(s) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to the +// next page of results. +func (r ServerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"servers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractServers interprets the results of a single page from a List() call, +// producing a slice of Server entities. +func ExtractServers(r pagination.Page) ([]Server, error) { + var s []Server + err := ExtractServersInto(r, &s) + return s, err +} + +// MetadataResult contains the result of a call for (potentially) multiple +// key-value pairs. Call its Extract method to interpret it as a +// map[string]interface. +type MetadataResult struct { + gophercloud.Result +} + +// GetMetadataResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadataResult struct { + MetadataResult +} + +// ResetMetadataResult contains the result of a Reset operation. Call its +// Extract method to interpret it as a map[string]interface. +type ResetMetadataResult struct { + MetadataResult +} + +// UpdateMetadataResult contains the result of an Update operation. Call its +// Extract method to interpret it as a map[string]interface. +type UpdateMetadataResult struct { + MetadataResult +} + +// MetadatumResult contains the result of a call for individual a single +// key-value pair. +type MetadatumResult struct { + gophercloud.Result +} + +// GetMetadatumResult contains the result of a Get operation. Call its Extract +// method to interpret it as a map[string]interface. +type GetMetadatumResult struct { + MetadatumResult +} + +// CreateMetadatumResult contains the result of a Create operation. Call its +// Extract method to interpret it as a map[string]interface. +type CreateMetadatumResult struct { + MetadatumResult +} + +// DeleteMetadatumResult contains the result of a Delete operation. Call its +// ExtractErr method to determine if the call succeeded or failed. +type DeleteMetadatumResult struct { + gophercloud.ErrResult +} + +// Extract interprets any MetadataResult as a Metadata, if possible. +func (r MetadataResult) Extract() (map[string]string, error) { + var s struct { + Metadata map[string]string `json:"metadata"` + } + err := r.ExtractInto(&s) + return s.Metadata, err +} + +// Extract interprets any MetadatumResult as a Metadatum, if possible. +func (r MetadatumResult) Extract() (map[string]string, error) { + var s struct { + Metadatum map[string]string `json:"meta"` + } + err := r.ExtractInto(&s) + return s.Metadatum, err +} + +// Address represents an IP address. +type Address struct { + Version int `json:"version"` + Address string `json:"addr"` +} + +// AddressPage abstracts the raw results of making a ListAddresses() request +// against the API. As OpenStack extensions may freely alter the response bodies +// of structures returned to the client, you may only safely access the data +// provided through the ExtractAddresses call. +type AddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if an AddressPage contains no networks. +func (r AddressPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + addresses, err := ExtractAddresses(r) + return len(addresses) == 0, err +} + +// ExtractAddresses interprets the results of a single page from a +// ListAddresses() call, producing a map of addresses. +func ExtractAddresses(r pagination.Page) (map[string][]Address, error) { + var s struct { + Addresses map[string][]Address `json:"addresses"` + } + err := (r.(AddressPage)).ExtractInto(&s) + return s.Addresses, err +} + +// NetworkAddressPage abstracts the raw results of making a +// ListAddressesByNetwork() request against the API. +// As OpenStack extensions may freely alter the response bodies of structures +// returned to the client, you may only safely access the data provided through +// the ExtractAddresses call. +type NetworkAddressPage struct { + pagination.SinglePageBase +} + +// IsEmpty returns true if a NetworkAddressPage contains no addresses. +func (r NetworkAddressPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + addresses, err := ExtractNetworkAddresses(r) + return len(addresses) == 0, err +} + +// ExtractNetworkAddresses interprets the results of a single page from a +// ListAddressesByNetwork() call, producing a slice of addresses. +func ExtractNetworkAddresses(r pagination.Page) ([]Address, error) { + var s map[string][]Address + err := (r.(NetworkAddressPage)).ExtractInto(&s) + if err != nil { + return nil, err + } + + var key string + for k := range s { + key = k + } + + return s[key], err +} + +// EvacuateResult is the response from an Evacuate operation. +// Call its ExtractAdminPass method to retrieve the admin password of the instance. +// The admin password will be an empty string if the cloud is not configured to inject admin passwords.. +type EvacuateResult struct { + gophercloud.Result +} + +func (r EvacuateResult) ExtractAdminPass() (string, error) { + var s struct { + AdminPass string `json:"adminPass"` + } + err := r.ExtractInto(&s) + if err != nil && err.Error() == "EOF" { + return "", nil + } + return s.AdminPass, err +} + +// InjectNetworkResult is the response of a InjectNetworkInfo operation. Call +// its ExtractErr method to determine if the request suceeded or failed. +type InjectNetworkResult struct { + gophercloud.ErrResult +} + +// LockResult and UnlockResult are the responses from a Lock and Unlock +// operations respectively. Call their ExtractErr methods to determine if the +// requests suceeded or failed. +type LockResult struct { + gophercloud.ErrResult +} + +type UnlockResult struct { + gophercloud.ErrResult +} + +// MigrateResult is the response from a Migrate operation. Call its ExtractErr +// method to determine if the request suceeded or failed. +type MigrateResult struct { + gophercloud.ErrResult +} + +// PauseResult is the response from a Pause operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type PauseResult struct { + gophercloud.ErrResult +} + +// UnpauseResult is the response from an Unpause operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type UnpauseResult struct { + gophercloud.ErrResult +} + +type commonResult struct { + gophercloud.Result +} + +// RescueResult is the response from a Rescue operation. Call its Extract +// method to retrieve adminPass for a rescued server. +type RescueResult struct { + commonResult +} + +// UnrescueResult is the response from an UnRescue operation. Call its ExtractErr +// method to determine if the call succeeded or failed. +type UnrescueResult struct { + gophercloud.ErrResult +} + +// Extract interprets any RescueResult as an AdminPass, if possible. +func (r RescueResult) Extract() (string, error) { + var s struct { + AdminPass string `json:"adminPass"` + } + err := r.ExtractInto(&s) + return s.AdminPass, err +} + +// ResetResult is the response of a ResetNetwork operation. Call its ExtractErr +// method to determine if the request suceeded or failed. +type ResetNetworkResult struct { + gophercloud.ErrResult +} + +// ResetResult is the response of a ResetState operation. Call its ExtractErr +// method to determine if the request suceeded or failed. +type ResetStateResult struct { + gophercloud.ErrResult +} + +// ShelveResult is the response from a Shelve operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type ShelveResult struct { + gophercloud.ErrResult +} + +// ShelveOffloadResult is the response from a Shelve operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type ShelveOffloadResult struct { + gophercloud.ErrResult +} + +// UnshelveResult is the response from Stop operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type UnshelveResult struct { + gophercloud.ErrResult +} + +// StartResult is the response from a Start operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StartResult struct { + gophercloud.ErrResult +} + +// StopResult is the response from Stop operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StopResult struct { + gophercloud.ErrResult +} + +// SuspendResult is the response from a Suspend operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type SuspendResult struct { + gophercloud.ErrResult +} + +// ResumeResult is the response from an Unsuspend operation. Call +// its ExtractErr method to determine if the request succeeded or failed. +type ResumeResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/urls.go new file mode 100644 index 000000000..36c8c90a1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/urls.go @@ -0,0 +1,51 @@ +package servers + +import "github.com/gophercloud/gophercloud/v2" + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers") +} + +func listURL(client *gophercloud.ServiceClient) string { + return createURL(client) +} + +func listDetailURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("servers", "detail") +} + +func deleteURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id) +} + +func getURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func updateURL(client *gophercloud.ServiceClient, id string) string { + return deleteURL(client, id) +} + +func actionURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "action") +} + +func metadatumURL(client *gophercloud.ServiceClient, id, key string) string { + return client.ServiceURL("servers", id, "metadata", key) +} + +func metadataURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "metadata") +} + +func listAddressesURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "ips") +} + +func listAddressesByNetworkURL(client *gophercloud.ServiceClient, id, network string) string { + return client.ServiceURL("servers", id, "ips", network) +} + +func passwordURL(client *gophercloud.ServiceClient, id string) string { + return client.ServiceURL("servers", id, "os-server-password") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/util.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/util.go new file mode 100644 index 000000000..4f611750d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers/util.go @@ -0,0 +1,24 @@ +package servers + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" +) + +// WaitForStatus will continually poll a server until it successfully +// transitions to a specified status. +func WaitForStatus(ctx context.Context, c *gophercloud.ServiceClient, id, status string) error { + return gophercloud.WaitFor(ctx, func(ctx context.Context) (bool, error) { + current, err := Get(ctx, c, id).Extract() + if err != nil { + return false, err + } + + if current.Status == status { + return true, nil + } + + return false, nil + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/doc.go new file mode 100644 index 000000000..4d89fb5d0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/doc.go @@ -0,0 +1,14 @@ +/* +Package openstack contains resources for the individual OpenStack projects +supported in Gophercloud. It also includes functions to authenticate to an +OpenStack cloud and for provisioning various service-level clients. + +Example of Creating a Service Client + + ao, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(context.TODO(), ao) + client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) +*/ +package openstack diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint.go new file mode 100644 index 000000000..617843442 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint.go @@ -0,0 +1,190 @@ +package openstack + +import ( + "context" + "regexp" + "slices" + "strconv" + + "github.com/gophercloud/gophercloud/v2" + tokens2 "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/v2/openstack/utils" +) + +var versionedServiceTypeAliasRegexp = regexp.MustCompile(`^.*v(\d)$`) + +func extractServiceTypeVersion(serviceType string) int { + matches := versionedServiceTypeAliasRegexp.FindAllStringSubmatch(serviceType, 1) + if matches != nil { + // no point converting to an int + ret, err := strconv.Atoi(matches[0][1]) + if err != nil { + return 0 + } + return ret + } + return 0 +} + +func endpointSupportsVersion(ctx context.Context, client *gophercloud.ProviderClient, serviceType, endpointURL string, expectedVersion int) (bool, error) { + // Swift doesn't support version discovery :( + if expectedVersion == 0 || serviceType == "object-store" { + return true, nil + } + + // Repeating verbatim from keystoneauth1 [1]: + // + // > The sins of our fathers become the blood on our hands. + // > If a user requests an old-style service type such as volumev2, then they + // > are inherently requesting the major API version 2. It's not a good + // > interface, but it's the one that was imposed on the world years ago + // > because the client libraries all hid the version discovery document. + // > In order to be able to ensure that a user who requests volumev2 does not + // > get a block-storage endpoint that only provides v3 of the block-storage + // > service, we need to pull the version out of the service_type. The + // > service-types-authority will prevent the growth of new monstrosities such + // > as this, but in order to move forward without breaking people, we have + // > to just cry in the corner while striking ourselves with thorned branches. + // > That said, for sure only do this hack for officially known service_types. + // + // So yeah, what mordred said. + // + // https://github.com/openstack/keystoneauth/blob/5.10.0/keystoneauth1/discover.py#L270-L290 + impliedVersion := extractServiceTypeVersion(serviceType) + if impliedVersion != 0 && impliedVersion != expectedVersion { + return false, nil + } + + // NOTE(stephenfin) In addition to the above, keystoneauth also supports a URL + // hack whereby it will extract the version from the URL. We may wish to + // implement this too. + + endpointURL, err := utils.BaseVersionedEndpoint(endpointURL) + if err != nil { + return false, err + } + + supportedVersions, err := utils.GetServiceVersions(ctx, client, endpointURL, false) + if err != nil { + return false, err + } + + for _, supportedVersion := range supportedVersions { + if supportedVersion.Major == expectedVersion { + return true, nil + } + } + + return false, nil +} + +/* +V2Endpoint discovers the endpoint URL for a specific service from a +ServiceCatalog acquired during the v2 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V2Endpoint(ctx context.Context, client *gophercloud.ProviderClient, catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. + for _, entry := range catalog.Entries { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region != "" && endpoint.Region != opts.Region { + continue + } + + var endpointURL string + switch opts.Availability { + case gophercloud.AvailabilityPublic: + endpointURL = gophercloud.NormalizeURL(endpoint.PublicURL) + case gophercloud.AvailabilityInternal: + endpointURL = gophercloud.NormalizeURL(endpoint.InternalURL) + case gophercloud.AvailabilityAdmin: + endpointURL = gophercloud.NormalizeURL(endpoint.AdminURL) + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + + endpointSupportsVersion, err := endpointSupportsVersion(ctx, client, entry.Type, endpointURL, opts.Version) + if err != nil { + return "", err + } + if !endpointSupportsVersion { + continue + } + + return endpointURL, nil + } + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} + +/* +V3Endpoint discovers the endpoint URL for a specific service from a Catalog +acquired during the v3 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V3Endpoint(ctx context.Context, client *gophercloud.ProviderClient, catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. + for _, entry := range catalog.Entries { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.Availability(endpoint.Interface) { + continue + } + if opts.Region != "" && endpoint.Region != opts.Region && endpoint.RegionID != opts.Region { + continue + } + + endpointURL := gophercloud.NormalizeURL(endpoint.URL) + + endpointSupportsVersion, err := endpointSupportsVersion(ctx, client, entry.Type, endpointURL, opts.Version) + if err != nil { + return "", err + } + if !endpointSupportsVersion { + continue + } + + return endpointURL, nil + } + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint_location.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint_location.go new file mode 100644 index 000000000..573c1f06f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/endpoint_location.go @@ -0,0 +1,103 @@ +package openstack + +import ( + "slices" + + "github.com/gophercloud/gophercloud/v2" + tokens2 "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens" + tokens3 "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" +) + +// TODO(stephenfin): Remove this module in v3. The functions below are no longer used. + +/* +V2EndpointURL discovers the endpoint URL for a specific service from a +ServiceCatalog acquired during the v2 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V2EndpointURL(catalog *tokens2.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Name if provided, and Region if provided. + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. + for _, entry := range catalog.Entries { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Region != "" && endpoint.Region != opts.Region { + continue + } + + var endpointURL string + switch opts.Availability { + case gophercloud.AvailabilityPublic: + endpointURL = gophercloud.NormalizeURL(endpoint.PublicURL) + case gophercloud.AvailabilityInternal: + endpointURL = gophercloud.NormalizeURL(endpoint.InternalURL) + case gophercloud.AvailabilityAdmin: + endpointURL = gophercloud.NormalizeURL(endpoint.AdminURL) + default: + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + + return endpointURL, nil + } + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} + +/* +V3EndpointURL discovers the endpoint URL for a specific service from a Catalog +acquired during the v3 identity service. + +The specified EndpointOpts are used to identify a unique, unambiguous endpoint +to return. It's an error both when multiple endpoints match the provided +criteria and when none do. The minimum that can be specified is a Type, but you +will also often need to specify a Name and/or a Region depending on what's +available on your OpenStack deployment. +*/ +func V3EndpointURL(catalog *tokens3.ServiceCatalog, opts gophercloud.EndpointOpts) (string, error) { + if opts.Availability != gophercloud.AvailabilityAdmin && + opts.Availability != gophercloud.AvailabilityPublic && + opts.Availability != gophercloud.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + // + // If multiple endpoints are found, we return the first result and disregard the rest. + // This behavior matches the Python library. See GH-1764. + for _, entry := range catalog.Entries { + if (slices.Contains(opts.Types(), entry.Type)) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != gophercloud.Availability(endpoint.Interface) { + continue + } + if opts.Region != "" && endpoint.Region != opts.Region && endpoint.RegionID != opts.Region { + continue + } + + return gophercloud.NormalizeURL(endpoint.URL), nil + } + } + } + + // Report an error if there were no matching endpoints. + err := &gophercloud.ErrEndpointNotFound{} + return "", err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/errors.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/errors.go new file mode 100644 index 000000000..f5273483e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/errors.go @@ -0,0 +1,47 @@ +package openstack + +import ( + "fmt" + + "github.com/gophercloud/gophercloud/v2" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ gophercloud.BaseError } + +func (e ErrEndpointNotFound) Error() string { + return "No suitable endpoint could be found in the service catalog." +} + +// ErrInvalidAvailabilityProvided is the error when an invalid endpoint +// availability is provided +type ErrInvalidAvailabilityProvided struct{ gophercloud.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoAuthURL) Error() string { + return "Environment variable OS_AUTH_URL needs to be set." +} + +// ErrNoUsername is the error when the OS_USERNAME environment variable is not +// found +type ErrNoUsername struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoUsername) Error() string { + return "Environment variable OS_USERNAME needs to be set." +} + +// ErrNoPassword is the error when the OS_PASSWORD environment variable is not +// found +type ErrNoPassword struct{ gophercloud.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/doc.go new file mode 100644 index 000000000..b14d69a56 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/doc.go @@ -0,0 +1,65 @@ +/* +Package tenants provides information and interaction with the +tenants API resource for the OpenStack Identity service. + +See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 +and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants +for more information. + +Example to List Tenants + + listOpts := &tenants.ListOpts{ + Limit: 2, + } + + allPages, err := tenants.List(identityClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allTenants, err := tenants.ExtractTenants(allPages) + if err != nil { + panic(err) + } + + for _, tenant := range allTenants { + fmt.Printf("%+v\n", tenant) + } + +Example to Create a Tenant + + createOpts := tenants.CreateOpts{ + Name: "tenant_name", + Description: "this is a tenant", + Enabled: gophercloud.Enabled, + } + + tenant, err := tenants.Create(context.TODO(), identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Tenant + + tenantID := "e6db6ed6277c461a853458589063b295" + + updateOpts := tenants.UpdateOpts{ + Description: "this is a new description", + Enabled: gophercloud.Disabled, + } + + tenant, err := tenants.Update(context.TODO(), identityClient, tenantID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Tenant + + tenantID := "e6db6ed6277c461a853458589063b295" + + err := tenants.Delete(context.TODO(), identitYClient, tenantID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package tenants diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/requests.go new file mode 100644 index 000000000..84a8b9df1 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/requests.go @@ -0,0 +1,134 @@ +package tenants + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToTenantListQuery() (string, error) +} + +// ListOpts filters the Tenants that are returned by the List call. +type ListOpts struct { + // Marker is the ID of the last Tenant on the previous page. + Marker string `q:"marker"` + + // Limit specifies the page size. + Limit int `q:"limit"` +} + +// ToTenantListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTenantListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List enumerates the Tenants to which the current token has access. +func List(client *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToTenantListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return TenantPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOpts represents the options needed when creating new tenant. +type CreateOpts struct { + // Name is the name of the tenant. + Name string `json:"name" required:"true"` + + // Description is the description of the tenant. + Description string `json:"description,omitempty"` + + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// CreateOptsBuilder enables extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToTenantCreateMap() (map[string]any, error) +} + +// ToTenantCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToTenantCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Create is the operation responsible for creating new tenant. +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToTenantCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get requests details on a single tenant by ID. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToTenantUpdateMap() (map[string]any, error) +} + +// UpdateOpts specifies the base attributes that may be updated on an existing +// tenant. +type UpdateOpts struct { + // Name is the name of the tenant. + Name string `json:"name,omitempty"` + + // Description is the description of the tenant. + Description *string `json:"description,omitempty"` + + // Enabled sets the tenant status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// ToTenantUpdateMap formats an UpdateOpts structure into a request body. +func (opts UpdateOpts) ToTenantUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "tenant") +} + +// Update is the operation responsible for updating exist tenants by their TenantID. +func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToTenantUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, updateURL(client, id), &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete is the operation responsible for permanently deleting a tenant. +func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(ctx, deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/results.go new file mode 100644 index 000000000..2569ffec0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/results.go @@ -0,0 +1,95 @@ +package tenants + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Tenant is a grouping of users in the identity service. +type Tenant struct { + // ID is a unique identifier for this tenant. + ID string `json:"id"` + + // Name is a friendlier user-facing name for this tenant. + Name string `json:"name"` + + // Description is a human-readable explanation of this Tenant's purpose. + Description string `json:"description"` + + // Enabled indicates whether or not a tenant is active. + Enabled bool `json:"enabled"` +} + +// TenantPage is a single page of Tenant results. +type TenantPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Tenants contains any results. +func (r TenantPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + tenants, err := ExtractTenants(r) + return len(tenants) == 0, err +} + +// NextPageURL extracts the "next" link from the tenants_links section of the result. +func (r TenantPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"tenants_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// ExtractTenants returns a slice of Tenants contained in a single page of +// results. +func ExtractTenants(r pagination.Page) ([]Tenant, error) { + var s struct { + Tenants []Tenant `json:"tenants"` + } + err := (r.(TenantPage)).ExtractInto(&s) + return s.Tenants, err +} + +type tenantResult struct { + gophercloud.Result +} + +// Extract interprets any tenantResults as a Tenant. +func (r tenantResult) Extract() (*Tenant, error) { + var s struct { + Tenant *Tenant `json:"tenant"` + } + err := r.ExtractInto(&s) + return s.Tenant, err +} + +// GetResult is the response from a Get request. Call its Extract method to +// interpret it as a Tenant. +type GetResult struct { + tenantResult +} + +// CreateResult is the response from a Create request. Call its Extract method +// to interpret it as a Tenant. +type CreateResult struct { + tenantResult +} + +// DeleteResult is the response from a Get request. Call its ExtractErr method +// to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult is the response from a Update request. Call its Extract method +// to interpret it as a Tenant. +type UpdateResult struct { + tenantResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/urls.go new file mode 100644 index 000000000..4c2aaf384 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants/urls.go @@ -0,0 +1,23 @@ +package tenants + +import "github.com/gophercloud/gophercloud/v2" + +func listURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func getURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func createURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tenants") +} + +func deleteURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} + +func updateURL(client *gophercloud.ServiceClient, tenantID string) string { + return client.ServiceURL("tenants", tenantID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/doc.go new file mode 100644 index 000000000..7cfc36730 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/doc.go @@ -0,0 +1,46 @@ +/* +Package tokens provides information and interaction with the token API +resource for the OpenStack Identity service. + +For more information, see: +http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2 + +Example to Create an Unscoped Token from a Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "pass" + } + + token, err := tokens.Create(context.TODO(), identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Tenant ID and Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "password", + TenantID: "fc394f2ab2df4114bde39905f800dc57" + } + + token, err := tokens.Create(context.TODO(), identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Tenant Name and Password + + authOpts := gophercloud.AuthOptions{ + Username: "user", + Password: "password", + TenantName: "tenantname" + } + + token, err := tokens.Create(context.TODO(), identityClient, authOpts).ExtractToken() + if err != nil { + panic(err) + } +*/ +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/requests.go new file mode 100644 index 000000000..5afa8fd26 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/requests.go @@ -0,0 +1,114 @@ +package tokens + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" +) + +// PasswordCredentialsV2 represents the required options to authenticate +// with a username and password. +type PasswordCredentialsV2 struct { + Username string `json:"username" required:"true"` + Password string `json:"password" required:"true"` +} + +// TokenCredentialsV2 represents the required options to authenticate +// with a token. +type TokenCredentialsV2 struct { + ID string `json:"id,omitempty" required:"true"` +} + +// AuthOptionsV2 wraps a gophercloud AuthOptions in order to adhere to the +// AuthOptionsBuilder interface. +type AuthOptionsV2 struct { + PasswordCredentials *PasswordCredentialsV2 `json:"passwordCredentials,omitempty" xor:"TokenCredentials"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // Some providers allow you to specify a TenantName instead of the TenantId. + // Some require both. Your provider's authentication policies will determine + // how these fields influence authentication. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + + // TokenCredentials allows users to authenticate (possibly as another user) + // with an authentication token ID. + TokenCredentials *TokenCredentialsV2 `json:"token,omitempty" xor:"PasswordCredentials"` +} + +// AuthOptionsBuilder allows extensions to add additional parameters to the +// token create request. +type AuthOptionsBuilder interface { + // ToTokenCreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV2CreateMap() (map[string]any, error) + CanReauth() bool +} + +// AuthOptions are the valid options for Openstack Identity v2 authentication. +// For field descriptions, see gophercloud.AuthOptions. +type AuthOptions struct { + IdentityEndpoint string `json:"-"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,omitempty"` + AllowReauth bool `json:"-"` + TokenID string +} + +// ToTokenV2CreateMap builds a token request body from the given AuthOptions. +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]any, error) { + v2Opts := AuthOptionsV2{ + TenantID: opts.TenantID, + TenantName: opts.TenantName, + } + + if opts.Password != "" { + v2Opts.PasswordCredentials = &PasswordCredentialsV2{ + Username: opts.Username, + Password: opts.Password, + } + } else { + v2Opts.TokenCredentials = &TokenCredentialsV2{ + ID: opts.TokenID, + } + } + + b, err := gophercloud.BuildRequestBody(v2Opts, "auth") + if err != nil { + return nil, err + } + return b, nil +} + +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +// Create authenticates to the identity service and attempts to acquire a Token. +// Generally, rather than interact with this call directly, end users should +// call openstack.AuthenticatedClient(), which abstracts all of the gory details +// about navigating service catalogs and such. +func Create(ctx context.Context, client *gophercloud.ServiceClient, auth AuthOptionsBuilder) (r CreateResult) { + b, err := auth.ToTokenV2CreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, CreateURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + OmitHeaders: []string{"X-Auth-Token"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get validates and retrieves information for user's token. +func Get(ctx context.Context, client *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := client.Get(ctx, GetURL(client, token), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/results.go new file mode 100644 index 000000000..516371ee8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/results.go @@ -0,0 +1,174 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants" +) + +// Token provides only the most basic information related to an authentication +// token. +type Token struct { + // ID provides the primary means of identifying a user to the OpenStack API. + // OpenStack defines this field as an opaque value, so do not depend on its + // content. It is safe, however, to compare for equality. + ID string + + // ExpiresAt provides a timestamp in ISO 8601 format, indicating when the + // authentication token becomes invalid. After this point in time, future + // API requests made using this authentication token will respond with + // errors. Either the caller will need to reauthenticate manually, or more + // preferably, the caller should exploit automatic re-authentication. + // See the AuthOptions structure for more details. + ExpiresAt time.Time + + // Tenant provides information about the tenant to which this token grants + // access. + Tenant tenants.Tenant +} + +// Role is a role for a user. +type Role struct { + Name string `json:"name"` +} + +// User is an OpenStack user. +type User struct { + ID string `json:"id"` + Name string `json:"name"` + UserName string `json:"username"` + Roles []Role `json:"roles"` +} + +// Endpoint represents a single API endpoint offered by a service. +// It provides the public and internal URLs, if supported, along with a region +// specifier, again if provided. +// +// The significance of the Region field will depend upon your provider. +// +// In addition, the interface offered by the service will have version +// information associated with it through the VersionId, VersionInfo, and +// VersionList fields, if provided or supported. +// +// In all cases, fields which aren't supported by the provider and service +// combined will assume a zero-value (""). +type Endpoint struct { + TenantID string `json:"tenantId"` + PublicURL string `json:"publicURL"` + InternalURL string `json:"internalURL"` + AdminURL string `json:"adminURL"` + Region string `json:"region"` + VersionID string `json:"versionId"` + VersionInfo string `json:"versionInfo"` + VersionList string `json:"versionList"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V2 service +// catalog listing. +// +// Each class of service, such as cloud DNS or block storage services, will have +// a single CatalogEntry representing it. +// +// Note: when looking for the desired service, try, whenever possible, to key +// off the type field. Otherwise, you'll tie the representation of the service +// to a specific provider. +type CatalogEntry struct { + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the + // service. Otherwise, for provider-specific services, the provider may assign + // their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry +} + +// CreateResult is the response from a Create request. Use ExtractToken() to +// interpret it as a Token, or ExtractServiceCatalog() to interpret it as a +// service catalog. +type CreateResult struct { + gophercloud.Result +} + +// GetResult is the deferred response from a Get call, which is the same with a +// Created token. Use ExtractUser() to interpret it as a User. +type GetResult struct { + CreateResult +} + +// ExtractToken returns the just-created Token from a CreateResult. +func (r CreateResult) ExtractToken() (*Token, error) { + var s struct { + Access struct { + Token struct { + Expires string `json:"expires"` + ID string `json:"id"` + Tenant tenants.Tenant `json:"tenant"` + } `json:"token"` + } `json:"access"` + } + + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + expiresTs, err := time.Parse(gophercloud.RFC3339Milli, s.Access.Token.Expires) + if err != nil { + return nil, err + } + + return &Token{ + ID: s.Access.Token.ID, + ExpiresAt: expiresTs, + Tenant: s.Access.Token.Tenant, + }, nil +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r CreateResult) ExtractTokenID() (string, error) { + var s struct { + Access struct { + Token struct { + ID string `json:"id"` + } `json:"token"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return s.Access.Token.ID, err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s struct { + Access struct { + Entries []CatalogEntry `json:"serviceCatalog"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &ServiceCatalog{Entries: s.Access.Entries}, err +} + +// ExtractUser returns the User from a GetResult. +func (r GetResult) ExtractUser() (*User, error) { + var s struct { + Access struct { + User User `json:"user"` + } `json:"access"` + } + err := r.ExtractInto(&s) + return &s.Access.User, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/urls.go new file mode 100644 index 000000000..845cdb58b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens/urls.go @@ -0,0 +1,13 @@ +package tokens + +import "github.com/gophercloud/gophercloud/v2" + +// CreateURL generates the URL used to create new Tokens. +func CreateURL(client *gophercloud.ServiceClient) string { + return client.ServiceURL("tokens") +} + +// GetURL generates the URL used to Validate Tokens. +func GetURL(client *gophercloud.ServiceClient, token string) string { + return client.ServiceURL("tokens", token) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/doc.go new file mode 100644 index 000000000..bd74473da --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/doc.go @@ -0,0 +1,40 @@ +/* +Package tokens provides information and interaction with the EC2 token API +resource for the OpenStack Identity service. + +For more information, see: +https://docs.openstack.org/api-ref/identity/v2-ext/ + +Example to Create a Token From an EC2 access and secret keys + + var authOptions tokens.AuthOptionsBuilder + authOptions = &ec2tokens.AuthOptions{ + Access: "a7f1e798b7c2417cba4a02de97dc3cdc", + Secret: "18f4f6761ada4e3795fa5273c30349b9", + } + + token, err := ec2tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to auth a client using EC2 access and secret keys + + client, err := openstack.NewClient("http://localhost:5000/v3") + if err != nil { + panic(err) + } + + var authOptions tokens.AuthOptionsBuilder + authOptions = &ec2tokens.AuthOptions{ + Access: "a7f1e798b7c2417cba4a02de97dc3cdc", + Secret: "18f4f6761ada4e3795fa5273c30349b9", + AllowReauth: true, + } + + err = openstack.AuthenticateV3(context.TODO(), client, authOptions, gophercloud.EndpointOpts{}) + if err != nil { + panic(err) + } +*/ +package ec2tokens diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/requests.go new file mode 100644 index 000000000..1d4cb5492 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/requests.go @@ -0,0 +1,376 @@ +package ec2tokens + +import ( + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha1" + "crypto/sha256" + "encoding/hex" + "fmt" + "net/url" + "sort" + "strings" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" +) + +const ( + // EC2CredentialsAwsRequestV4 is a constant, used to generate AWS + // Credential V4. + EC2CredentialsAwsRequestV4 = "aws4_request" + // EC2CredentialsHmacSha1V2 is a HMAC SHA1 signature method. Used to + // generate AWS Credential V2. + EC2CredentialsHmacSha1V2 = "HmacSHA1" + // EC2CredentialsHmacSha256V2 is a HMAC SHA256 signature method. Used + // to generate AWS Credential V2. + EC2CredentialsHmacSha256V2 = "HmacSHA256" + // EC2CredentialsAwsHmacV4 is an AWS signature V4 signing method. + // More details: + // https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html + EC2CredentialsAwsHmacV4 = "AWS4-HMAC-SHA256" + // EC2CredentialsTimestampFormatV4 is an AWS signature V4 timestamp + // format. + EC2CredentialsTimestampFormatV4 = "20060102T150405Z" + // EC2CredentialsDateFormatV4 is an AWS signature V4 date format. + EC2CredentialsDateFormatV4 = "20060102" +) + +// AuthOptions represents options for authenticating a user using EC2 credentials. +type AuthOptions struct { + // Access is the EC2 Credential Access ID. + Access string `json:"access" required:"true"` + // Secret is the EC2 Credential Secret, used to calculate signature. + // Not used, when a Signature is is. + Secret string `json:"-"` + // Host is a HTTP request Host header. Used to calculate an AWS + // signature V2. For signature V4 set the Host inside Headers map. + // Optional. + Host string `json:"host"` + // Path is a HTTP request path. Optional. + Path string `json:"path"` + // Verb is a HTTP request method. Optional. + Verb string `json:"verb"` + // Headers is a map of HTTP request headers. Optional. + Headers map[string]string `json:"headers"` + // Region is a region name to calculate an AWS signature V4. Optional. + Region string `json:"-"` + // Service is a service name to calculate an AWS signature V4. Optional. + Service string `json:"-"` + // Params is a map of GET method parameters. Optional. + Params map[string]string `json:"params"` + // AllowReauth allows Gophercloud to re-authenticate automatically + // if/when your token expires. + AllowReauth bool `json:"-"` + // Signature can be either a []byte (encoded to base64 automatically) or + // a string. You can set the singature explicitly, when you already know + // it. In this case default Params won't be automatically set. Optional. + Signature any `json:"signature"` + // BodyHash is a HTTP request body sha256 hash. When nil and Signature + // is not set, a random hash is generated. Optional. + BodyHash *string `json:"body_hash"` + // Timestamp is a timestamp to calculate a V4 signature. Optional. + Timestamp *time.Time `json:"-"` + // Token is a []byte string (encoded to base64 automatically) which was + // signed by an EC2 secret key. Used by S3 tokens for validation only. + // Token must be set with a Signature. If a Signature is not provided, + // a Token will be generated automatically along with a Signature. + Token []byte `json:"token,omitempty"` +} + +// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string +// for an AWS signature V2. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L133 +func EC2CredentialsBuildCanonicalQueryStringV2(params map[string]string) string { + var keys []string + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + var pairs []string + for _, k := range keys { + pairs = append(pairs, fmt.Sprintf("%s=%s", k, url.QueryEscape(params[k]))) + } + + return strings.Join(pairs, "&") +} + +// EC2CredentialsBuildStringToSignV2 builds a string to sign an AWS signature +// V2. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L148 +func EC2CredentialsBuildStringToSignV2(opts AuthOptions) []byte { + stringToSign := strings.Join([]string{ + opts.Verb, + opts.Host, + opts.Path, + }, "\n") + + return []byte(strings.Join([]string{ + stringToSign, + EC2CredentialsBuildCanonicalQueryStringV2(opts.Params), + }, "\n")) +} + +// EC2CredentialsBuildCanonicalQueryStringV2 builds a canonical query string +// for an AWS signature V4. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L244 +func EC2CredentialsBuildCanonicalQueryStringV4(verb string, params map[string]string) string { + if verb == "POST" { + return "" + } + return EC2CredentialsBuildCanonicalQueryStringV2(params) +} + +// EC2CredentialsBuildCanonicalHeadersV4 builds a canonical string based on +// "headers" map and "signedHeaders" string parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L216 +func EC2CredentialsBuildCanonicalHeadersV4(headers map[string]string, signedHeaders string) string { + headersLower := make(map[string]string, len(headers)) + for k, v := range headers { + headersLower[strings.ToLower(k)] = v + } + + var headersList []string + for _, h := range strings.Split(signedHeaders, ";") { + if v, ok := headersLower[h]; ok { + headersList = append(headersList, h+":"+v) + } + } + + return strings.Join(headersList, "\n") + "\n" +} + +// EC2CredentialsBuildSignatureKeyV4 builds a HMAC 256 signature key based on +// input parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L169 +func EC2CredentialsBuildSignatureKeyV4(secret, region, service string, date time.Time) []byte { + kDate := sumHMAC256([]byte("AWS4"+secret), []byte(date.Format(EC2CredentialsDateFormatV4))) + kRegion := sumHMAC256(kDate, []byte(region)) + kService := sumHMAC256(kRegion, []byte(service)) + return sumHMAC256(kService, []byte(EC2CredentialsAwsRequestV4)) +} + +// EC2CredentialsBuildStringToSignV4 builds an AWS v4 signature string to sign +// based on input parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L251 +func EC2CredentialsBuildStringToSignV4(opts AuthOptions, signedHeaders string, bodyHash string, date time.Time) []byte { + scope := strings.Join([]string{ + date.Format(EC2CredentialsDateFormatV4), + opts.Region, + opts.Service, + EC2CredentialsAwsRequestV4, + }, "/") + + canonicalRequest := strings.Join([]string{ + opts.Verb, + opts.Path, + EC2CredentialsBuildCanonicalQueryStringV4(opts.Verb, opts.Params), + EC2CredentialsBuildCanonicalHeadersV4(opts.Headers, signedHeaders), + signedHeaders, + bodyHash, + }, "\n") + hash := sha256.Sum256([]byte(canonicalRequest)) + + return []byte(strings.Join([]string{ + EC2CredentialsAwsHmacV4, + date.Format(EC2CredentialsTimestampFormatV4), + scope, + hex.EncodeToString(hash[:]), + }, "\n")) +} + +// EC2CredentialsBuildSignatureV4 builds an AWS v4 signature based on input +// parameters. +// https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L285..L286 +func EC2CredentialsBuildSignatureV4(key []byte, stringToSign []byte) string { + return hex.EncodeToString(sumHMAC256(key, stringToSign)) +} + +// EC2CredentialsBuildAuthorizationHeaderV4 builds an AWS v4 Authorization +// header based on auth parameters, date and signature +func EC2CredentialsBuildAuthorizationHeaderV4(opts AuthOptions, signedHeaders string, signature string, date time.Time) string { + return fmt.Sprintf("%s Credential=%s/%s/%s/%s/%s, SignedHeaders=%s, Signature=%s", + EC2CredentialsAwsHmacV4, + opts.Access, + date.Format(EC2CredentialsDateFormatV4), + opts.Region, + opts.Service, + EC2CredentialsAwsRequestV4, + signedHeaders, + signature) +} + +// ToTokenV3ScopeMap is a dummy method to satisfy tokens.AuthOptionsBuilder +// interface. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]any, error) { + return nil, nil +} + +// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package. +func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]any) (map[string]string, error) { + return nil, nil +} + +// CanReauth is a method method to satisfy tokens.AuthOptionsBuilder interface +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +// ToTokenV3CreateMap formats an AuthOptions into a create request. +func (opts *AuthOptions) ToTokenV3CreateMap(map[string]any) (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "credentials") + if err != nil { + return nil, err + } + + if opts.Signature != nil { + return b, nil + } + + // calculate signature, when it is not set + c, _ := b["credentials"].(map[string]any) + h := interfaceToMap(c, "headers") + p := interfaceToMap(c, "params") + + // detect and process a signature v2 + if v, ok := p["SignatureVersion"]; ok && v == "2" { + delete(c, "body_hash") + delete(c, "headers") + if v, ok := p["SignatureMethod"]; ok { + // params is a map of strings + strToSign := EC2CredentialsBuildStringToSignV2(*opts) + switch v { + case EC2CredentialsHmacSha1V2: + // keystone uses this method only when HmacSHA256 is not available on the server side + // https://github.com/openstack/python-keystoneclient/blob/stable/train/keystoneclient/contrib/ec2/utils.py#L151..L156 + c["signature"] = sumHMAC1([]byte(opts.Secret), strToSign) + return b, nil + case EC2CredentialsHmacSha256V2: + c["signature"] = sumHMAC256([]byte(opts.Secret), strToSign) + return b, nil + } + return nil, fmt.Errorf("unsupported signature method: %s", v) + } + return nil, fmt.Errorf("signature method must be provided") + } else if ok { + return nil, fmt.Errorf("unsupported signature version: %s", v) + } + + // it is not a signature v2, but a signature v4 + date := time.Now().UTC() + if opts.Timestamp != nil { + date = *opts.Timestamp + } + if v := c["body_hash"]; v == nil { + // when body_hash is not set, generate a random one + bodyHash, err := randomBodyHash() + if err != nil { + return nil, fmt.Errorf("failed to generate random hash") + } + c["body_hash"] = bodyHash + } + + signedHeaders := h["X-Amz-SignedHeaders"] + + stringToSign := EC2CredentialsBuildStringToSignV4(*opts, signedHeaders, c["body_hash"].(string), date) + key := EC2CredentialsBuildSignatureKeyV4(opts.Secret, opts.Region, opts.Service, date) + c["signature"] = EC2CredentialsBuildSignatureV4(key, stringToSign) + h["X-Amz-Date"] = date.Format(EC2CredentialsTimestampFormatV4) + h["Authorization"] = EC2CredentialsBuildAuthorizationHeaderV4(*opts, signedHeaders, c["signature"].(string), date) + + // token is only used for S3 tokens validation and will be removed when using EC2 validation + c["token"] = stringToSign + + return b, nil +} + +// Create authenticates and either generates a new token from EC2 credentials +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + // delete "token" element, since it is used in s3tokens + deleteBodyElements(b, "token") + + resp, err := c.Post(ctx, ec2tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ValidateS3Token authenticates an S3 request using EC2 credentials. Doesn't +// generate a new token ID, but returns a tokens.CreateResult. +func ValidateS3Token(ctx context.Context, c *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + // delete unused element, since it is used in ec2tokens only + deleteBodyElements(b, "body_hash", "headers", "host", "params", "path", "verb") + + resp, err := c.Post(ctx, s3tokensURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// The following are small helper functions used to help build the signature. + +// sumHMAC1 is a func to implement the HMAC SHA1 signature method. +func sumHMAC1(key []byte, data []byte) []byte { + hash := hmac.New(sha1.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// sumHMAC256 is a func to implement the HMAC SHA256 signature method. +func sumHMAC256(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +// randomBodyHash is a func to generate a random sha256 hexdigest. +func randomBodyHash() (string, error) { + h := make([]byte, 64) + if _, err := rand.Read(h); err != nil { + return "", err + } + return hex.EncodeToString(h), nil +} + +// interfaceToMap is a func used to represent a "credentials" map element as a +// "map[string]string" +func interfaceToMap(c map[string]any, key string) map[string]string { + // convert map[string]any to map[string]string + m := make(map[string]string) + if v, _ := c[key].(map[string]any); v != nil { + for k, v := range v { + m[k] = v.(string) + } + } + + c[key] = m + + return m +} + +// deleteBodyElements deletes map body elements +func deleteBodyElements(b map[string]any, elements ...string) { + if c, ok := b["credentials"].(map[string]any); ok { + for _, k := range elements { + delete(c, k) + } + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/urls.go new file mode 100644 index 000000000..91add91eb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens/urls.go @@ -0,0 +1,11 @@ +package ec2tokens + +import "github.com/gophercloud/gophercloud/v2" + +func ec2tokensURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ec2tokens") +} + +func s3tokensURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("s3tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/doc.go new file mode 100644 index 000000000..c0cfa924a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/doc.go @@ -0,0 +1,122 @@ +/* +Package oauth1 enables management of OpenStack OAuth1 tokens and Authentication. + +Example to Create an OAuth1 Consumer + + createConsumerOpts := oauth1.CreateConsumerOpts{ + Description: "My consumer", + } + consumer, err := oauth1.CreateConsumer(context.TODO(), identityClient, createConsumerOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Consumer secret is available only on create response + fmt.Printf("Consumer: %+v\n", consumer) + +Example to Request an unauthorized OAuth1 token + + requestTokenOpts := oauth1.RequestTokenOpts{ + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + OAuthSignatureMethod: oauth1.HMACSHA1, + RequestedProjectID: projectID, + } + requestToken, err := oauth1.RequestToken(context.TODO(), identityClient, requestTokenOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Request token secret is available only on request response + fmt.Printf("Request token: %+v\n", requestToken) + +Example to Authorize an unauthorized OAuth1 token + + authorizeTokenOpts := oauth1.AuthorizeTokenOpts{ + Roles: []oauth1.Role{ + {Name: "member"}, + }, + } + authToken, err := oauth1.AuthorizeToken(context.TODO(), identityClient, requestToken.OAuthToken, authorizeTokenOpts).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("Verifier ID of the unauthorized Token: %+v\n", authToken.OAuthVerifier) + +Example to Create an OAuth1 Access Token + + accessTokenOpts := oauth1.CreateAccessTokenOpts{ + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + OAuthToken: requestToken.OAuthToken, + OAuthTokenSecret: requestToken.OAuthTokenSecret, + OAuthVerifier: authToken.OAuthVerifier, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + accessToken, err := oauth1.CreateAccessToken(context.TODO(), identityClient, accessTokenOpts).Extract() + if err != nil { + panic(err) + } + + // NOTE: Access token secret is available only on create response + fmt.Printf("OAuth1 Access Token: %+v\n", accessToken) + +Example to List User's OAuth1 Access Tokens + + allPages, err := oauth1.ListAccessTokens(identityClient, userID).AllPages(context.TODO()) + if err != nil { + panic(err) + } + accessTokens, err := oauth1.ExtractAccessTokens(allPages) + if err != nil { + panic(err) + } + + for _, accessToken := range accessTokens { + fmt.Printf("Access Token: %+v\n", accessToken) + } + +Example to Authenticate a client using OAuth1 method + + client, err := openstack.NewClient("http://localhost:5000/v3") + if err != nil { + panic(err) + } + + authOptions := &oauth1.AuthOptions{ + // consumer token, created earlier + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + // access token, created earlier + OAuthToken: accessToken.OAuthToken, + OAuthTokenSecret: accessToken.OAuthTokenSecret, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + err = openstack.AuthenticateV3(context.TODO(), client, authOptions, gophercloud.EndpointOpts{}) + if err != nil { + panic(err) + } + +Example to Create a Token using OAuth1 method + + var oauth1Token struct { + tokens.Token + oauth1.TokenExt + } + + createOpts := &oauth1.AuthOptions{ + // consumer token, created earlier + OAuthConsumerKey: consumer.ID, + OAuthConsumerSecret: consumer.Secret, + // access token, created earlier + OAuthToken: accessToken.OAuthToken, + OAuthTokenSecret: accessToken.OAuthTokenSecret, + OAuthSignatureMethod: oauth1.HMACSHA1, + } + err := tokens.Create(context.TODO(), identityClient, createOpts).ExtractInto(&oauth1Token) + if err != nil { + panic(err) + } +*/ +package oauth1 diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/requests.go new file mode 100644 index 000000000..0b23269ff --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/requests.go @@ -0,0 +1,594 @@ +package oauth1 + +import ( + "context" + "crypto/hmac" + "crypto/sha1" + "encoding/base64" + "fmt" + "io" + "math/rand" + "net/url" + "sort" + "strconv" + "strings" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Type SignatureMethod is a OAuth1 SignatureMethod type. +type SignatureMethod string + +const ( + // HMACSHA1 is a recommended OAuth1 signature method. + HMACSHA1 SignatureMethod = "HMAC-SHA1" + + // PLAINTEXT signature method is not recommended to be used in + // production environment. + PLAINTEXT SignatureMethod = "PLAINTEXT" + + // OAuth1TokenContentType is a supported content type for an OAuth1 + // token. + OAuth1TokenContentType = "application/x-www-form-urlencoded" +) + +// AuthOptions represents options for authenticating a user using OAuth1 tokens. +type AuthOptions struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthToken is the OAuth1 Request Token. + OAuthToken string `q:"oauth_token" required:"true"` + + // OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate + // an OAuth1 request signature. + OAuthTokenSecret string `required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` + + // AllowReauth allows Gophercloud to re-authenticate automatically + // if/when your token expires. + AllowReauth bool +} + +// ToTokenV3HeadersMap builds the headers required for an OAuth1-based create +// request. +func (opts AuthOptions) ToTokenV3HeadersMap(headerOpts map[string]any) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "") + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret} + + method := headerOpts["method"].(string) + u := headerOpts["url"].(string) + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + + authHeader := buildAuthHeader(q.Query(), signature) + + headers := map[string]string{ + "Authorization": authHeader, + "X-Auth-Token": "", + } + + return headers, nil +} + +// ToTokenV3ScopeMap allows AuthOptions to satisfy the tokens.AuthOptionsBuilder +// interface. +func (opts AuthOptions) ToTokenV3ScopeMap() (map[string]any, error) { + return nil, nil +} + +// CanReauth allows AuthOptions to satisfy the tokens.AuthOptionsBuilder +// interface. +func (opts AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +// ToTokenV3CreateMap builds a create request body. +func (opts AuthOptions) ToTokenV3CreateMap(map[string]any) (map[string]any, error) { + // identityReq defines the "identity" portion of an OAuth1-based authentication + // create request body. + type identityReq struct { + Methods []string `json:"methods"` + OAuth1 struct{} `json:"oauth1"` + } + + // authReq defines the "auth" portion of an OAuth1-based authentication + // create request body. + type authReq struct { + Identity identityReq `json:"identity"` + } + + // oauth1Request defines how an OAuth1-based authentication create + // request body looks. + type oauth1Request struct { + Auth authReq `json:"auth"` + } + + var req oauth1Request + + req.Auth.Identity.Methods = []string{"oauth1"} + return gophercloud.BuildRequestBody(req, "") +} + +// Create authenticates and either generates a new OpenStack token +// from an OAuth1 token. +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts tokens.AuthOptionsBuilder) (r tokens.CreateResult) { + b, err := opts.ToTokenV3CreateMap(nil) + if err != nil { + r.Err = err + return + } + + headerOpts := map[string]any{ + "method": "POST", + "url": authURL(client), + } + + h, err := opts.ToTokenV3HeadersMap(headerOpts) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(ctx, authURL(client), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateConsumerOptsBuilder allows extensions to add additional parameters to +// the CreateConsumer request. +type CreateConsumerOptsBuilder interface { + ToOAuth1CreateConsumerMap() (map[string]any, error) +} + +// CreateConsumerOpts provides options used to create a new Consumer. +type CreateConsumerOpts struct { + // Description is the consumer description. + Description string `json:"description"` +} + +// ToOAuth1CreateConsumerMap formats a CreateConsumerOpts into a create request. +func (opts CreateConsumerOpts) ToOAuth1CreateConsumerMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "consumer") +} + +// CreateConsumer creates a new Consumer. +func CreateConsumer(ctx context.Context, client *gophercloud.ServiceClient, opts CreateConsumerOptsBuilder) (r CreateConsumerResult) { + b, err := opts.ToOAuth1CreateConsumerMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, consumersURL(client), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteConsumer deletes a Consumer. +func DeleteConsumer(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteConsumerResult) { + resp, err := client.Delete(ctx, consumerURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// List enumerates Consumers. +func ListConsumers(client *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(client, consumersURL(client), func(r pagination.PageResult) pagination.Page { + return ConsumersPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetConsumer retrieves details on a single Consumer by ID. +func GetConsumer(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetConsumerResult) { + resp, err := client.Get(ctx, consumerURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateConsumerOptsBuilder allows extensions to add additional parameters to the +// UpdateConsumer request. +type UpdateConsumerOptsBuilder interface { + ToOAuth1UpdateConsumerMap() (map[string]any, error) +} + +// UpdateConsumerOpts provides options used to update a consumer. +type UpdateConsumerOpts struct { + // Description is the consumer description. + Description string `json:"description"` +} + +// ToOAuth1UpdateConsumerMap formats an UpdateConsumerOpts into a consumer update +// request. +func (opts UpdateConsumerOpts) ToOAuth1UpdateConsumerMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "consumer") +} + +// UpdateConsumer updates an existing Consumer. +func UpdateConsumer(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateConsumerOptsBuilder) (r UpdateConsumerResult) { + b, err := opts.ToOAuth1UpdateConsumerMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Patch(ctx, consumerURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RequestTokenOptsBuilder allows extensions to add additional parameters to the +// RequestToken request. +type RequestTokenOptsBuilder interface { + ToOAuth1RequestTokenHeaders(string, string) (map[string]string, error) +} + +// RequestTokenOpts provides options used to get a consumer unauthorized +// request token. +type RequestTokenOpts struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` + + // RequestedProjectID is a Project ID a consumer user requested an + // access to. + RequestedProjectID string `h:"Requested-Project-Id"` +} + +// ToOAuth1RequestTokenHeaders formats a RequestTokenOpts into a map of request +// headers. +func (opts RequestTokenOpts) ToOAuth1RequestTokenHeaders(method, u string) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "oob") + if err != nil { + return nil, err + } + + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret} + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + authHeader := buildAuthHeader(q.Query(), signature) + + h["Authorization"] = authHeader + + return h, nil +} + +// RequestToken requests an unauthorized OAuth1 Token. +func RequestToken(ctx context.Context, client *gophercloud.ServiceClient, opts RequestTokenOptsBuilder) (r TokenResult) { + h, err := opts.ToOAuth1RequestTokenHeaders("POST", requestTokenURL(client)) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(ctx, requestTokenURL(client), nil, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + KeepResponseBody: true, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + if r.Err != nil { + return + } + defer resp.Body.Close() + if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType { + r.Err = fmt.Errorf("unsupported Content-Type: %q", v) + return + } + r.Body, r.Err = io.ReadAll(resp.Body) + return +} + +// AuthorizeTokenOptsBuilder allows extensions to add additional parameters to +// the AuthorizeToken request. +type AuthorizeTokenOptsBuilder interface { + ToOAuth1AuthorizeTokenMap() (map[string]any, error) +} + +// AuthorizeTokenOpts provides options used to authorize a request token. +type AuthorizeTokenOpts struct { + Roles []Role `json:"roles"` +} + +// Role is a struct representing a role object in a AuthorizeTokenOpts struct. +type Role struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +// ToOAuth1AuthorizeTokenMap formats an AuthorizeTokenOpts into an authorize token +// request. +func (opts AuthorizeTokenOpts) ToOAuth1AuthorizeTokenMap() (map[string]any, error) { + for _, r := range opts.Roles { + if r == (Role{}) { + return nil, fmt.Errorf("role must not be empty") + } + } + return gophercloud.BuildRequestBody(opts, "") +} + +// AuthorizeToken authorizes an unauthorized consumer token. +func AuthorizeToken(ctx context.Context, client *gophercloud.ServiceClient, id string, opts AuthorizeTokenOptsBuilder) (r AuthorizeTokenResult) { + b, err := opts.ToOAuth1AuthorizeTokenMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, authorizeTokenURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateAccessTokenOptsBuilder allows extensions to add additional parameters +// to the CreateAccessToken request. +type CreateAccessTokenOptsBuilder interface { + ToOAuth1CreateAccessTokenHeaders(string, string) (map[string]string, error) +} + +// CreateAccessTokenOpts provides options used to create an OAuth1 token. +type CreateAccessTokenOpts struct { + // OAuthConsumerKey is the OAuth1 Consumer Key. + OAuthConsumerKey string `q:"oauth_consumer_key" required:"true"` + + // OAuthConsumerSecret is the OAuth1 Consumer Secret. Used to generate + // an OAuth1 request signature. + OAuthConsumerSecret string `required:"true"` + + // OAuthToken is the OAuth1 Request Token. + OAuthToken string `q:"oauth_token" required:"true"` + + // OAuthTokenSecret is the OAuth1 Request Token Secret. Used to generate + // an OAuth1 request signature. + OAuthTokenSecret string `required:"true"` + + // OAuthVerifier is the OAuth1 verification code. + OAuthVerifier string `q:"oauth_verifier" required:"true"` + + // OAuthSignatureMethod is the OAuth1 signature method the Consumer used + // to sign the request. Supported values are "HMAC-SHA1" or "PLAINTEXT". + // "PLAINTEXT" is not recommended for production usage. + OAuthSignatureMethod SignatureMethod `q:"oauth_signature_method" required:"true"` + + // OAuthTimestamp is an OAuth1 request timestamp. If nil, current Unix + // timestamp will be used. + OAuthTimestamp *time.Time + + // OAuthNonce is an OAuth1 request nonce. Nonce must be a random string, + // uniquely generated for each request. Will be generated automatically + // when it is not set. + OAuthNonce string `q:"oauth_nonce"` +} + +// ToOAuth1CreateAccessTokenHeaders formats a CreateAccessTokenOpts into a map of +// request headers. +func (opts CreateAccessTokenOpts) ToOAuth1CreateAccessTokenHeaders(method, u string) (map[string]string, error) { + q, err := buildOAuth1QueryString(opts, opts.OAuthTimestamp, "") + if err != nil { + return nil, err + } + + signatureKeys := []string{opts.OAuthConsumerSecret, opts.OAuthTokenSecret} + stringToSign := buildStringToSign(method, u, q.Query()) + signature := url.QueryEscape(signString(opts.OAuthSignatureMethod, stringToSign, signatureKeys)) + authHeader := buildAuthHeader(q.Query(), signature) + + headers := map[string]string{ + "Authorization": authHeader, + } + + return headers, nil +} + +// CreateAccessToken creates a new OAuth1 Access Token +func CreateAccessToken(ctx context.Context, client *gophercloud.ServiceClient, opts CreateAccessTokenOptsBuilder) (r TokenResult) { + h, err := opts.ToOAuth1CreateAccessTokenHeaders("POST", createAccessTokenURL(client)) + if err != nil { + r.Err = err + return + } + + resp, err := client.Post(ctx, createAccessTokenURL(client), nil, nil, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{201}, + KeepResponseBody: true, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + if r.Err != nil { + return + } + defer resp.Body.Close() + if v := r.Header.Get("Content-Type"); v != OAuth1TokenContentType { + r.Err = fmt.Errorf("unsupported Content-Type: %q", v) + return + } + r.Body, r.Err = io.ReadAll(resp.Body) + return +} + +// GetAccessToken retrieves details on a single OAuth1 access token by an ID. +func GetAccessToken(ctx context.Context, client *gophercloud.ServiceClient, userID string, id string) (r GetAccessTokenResult) { + resp, err := client.Get(ctx, userAccessTokenURL(client, userID, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RevokeAccessToken revokes an OAuth1 access token. +func RevokeAccessToken(ctx context.Context, client *gophercloud.ServiceClient, userID string, id string) (r RevokeAccessTokenResult) { + resp, err := client.Delete(ctx, userAccessTokenURL(client, userID, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAccessTokens enumerates authorized access tokens. +func ListAccessTokens(client *gophercloud.ServiceClient, userID string) pagination.Pager { + url := userAccessTokensURL(client, userID) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessTokensPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// ListAccessTokenRoles enumerates authorized access token roles. +func ListAccessTokenRoles(client *gophercloud.ServiceClient, userID string, id string) pagination.Pager { + url := userAccessTokenRolesURL(client, userID, id) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return AccessTokenRolesPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetAccessTokenRole retrieves details on a single OAuth1 access token role by +// an ID. +func GetAccessTokenRole(ctx context.Context, client *gophercloud.ServiceClient, userID string, id string, roleID string) (r GetAccessTokenRoleResult) { + resp, err := client.Get(ctx, userAccessTokenRoleURL(client, userID, id, roleID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// The following are small helper functions used to help build the signature. + +// buildOAuth1QueryString builds a URLEncoded parameters string specific for +// OAuth1-based requests. +func buildOAuth1QueryString(opts any, timestamp *time.Time, callback string) (*url.URL, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return nil, err + } + + query := q.Query() + + if timestamp != nil { + // use provided timestamp + query.Set("oauth_timestamp", strconv.FormatInt(timestamp.Unix(), 10)) + } else { + // use current timestamp + query.Set("oauth_timestamp", strconv.FormatInt(time.Now().UTC().Unix(), 10)) + } + + if query.Get("oauth_nonce") == "" { + // when nonce is not set, generate a random one + query.Set("oauth_nonce", strconv.FormatInt(rand.Int63(), 10)+query.Get("oauth_timestamp")) + } + + if callback != "" { + query.Set("oauth_callback", callback) + } + query.Set("oauth_version", "1.0") + + return &url.URL{RawQuery: query.Encode()}, nil +} + +// buildStringToSign builds a string to be signed. +func buildStringToSign(method string, u string, query url.Values) []byte { + parsedURL, _ := url.Parse(u) + p := parsedURL.Port() + s := parsedURL.Scheme + + // Default scheme port must be stripped + if s == "http" && p == "80" || s == "https" && p == "443" { + parsedURL.Host = strings.TrimSuffix(parsedURL.Host, ":"+p) + } + + // Ensure that URL doesn't contain queries + parsedURL.RawQuery = "" + + v := strings.Join( + []string{method, url.QueryEscape(parsedURL.String()), url.QueryEscape(query.Encode())}, "&") + + return []byte(v) +} + +// signString signs a string using an OAuth1 signature method. +func signString(signatureMethod SignatureMethod, strToSign []byte, signatureKeys []string) string { + var key []byte + for i, k := range signatureKeys { + key = append(key, []byte(url.QueryEscape(k))...) + if i == 0 { + key = append(key, '&') + } + } + + var signedString string + switch signatureMethod { + case PLAINTEXT: + signedString = string(key) + default: + h := hmac.New(sha1.New, key) + h.Write(strToSign) + signedString = base64.StdEncoding.EncodeToString(h.Sum(nil)) + } + + return signedString +} + +// buildAuthHeader generates an OAuth1 Authorization header with a signature +// calculated using an OAuth1 signature method. +func buildAuthHeader(query url.Values, signature string) string { + var authHeader []string + var keys []string + for k := range query { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + for _, v := range query[k] { + authHeader = append(authHeader, fmt.Sprintf("%s=%q", k, url.QueryEscape(v))) + } + } + + authHeader = append(authHeader, fmt.Sprintf("oauth_signature=%q", signature)) + + return "OAuth " + strings.Join(authHeader, ", ") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/results.go new file mode 100644 index 000000000..2ed75bd74 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/results.go @@ -0,0 +1,317 @@ +package oauth1 + +import ( + "encoding/json" + "net/url" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Consumer represents a delegated authorization request between two +// identities. +type Consumer struct { + ID string `json:"id"` + Secret string `json:"secret"` + Description string `json:"description"` +} + +type consumerResult struct { + gophercloud.Result +} + +// CreateConsumerResult is the response from a Create operation. Call its +// Extract method to interpret it as a Consumer. +type CreateConsumerResult struct { + consumerResult +} + +// UpdateConsumerResult is the response from a Create operation. Call its +// Extract method to interpret it as a Consumer. +type UpdateConsumerResult struct { + consumerResult +} + +// DeleteConsumerResult is the response from a Delete operation. Call its +// ExtractErr to determine if the request succeeded or failed. +type DeleteConsumerResult struct { + gophercloud.ErrResult +} + +// ConsumersPage is a single page of Region results. +type ConsumersPage struct { + pagination.LinkedPageBase +} + +// GetConsumerResult is the response from a Get operation. Call its Extract +// method to interpret it as a Consumer. +type GetConsumerResult struct { + consumerResult +} + +// IsEmpty determines whether or not a page of Consumers contains any results. +func (c ConsumersPage) IsEmpty() (bool, error) { + if c.StatusCode == 204 { + return true, nil + } + + consumers, err := ExtractConsumers(c) + return len(consumers) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (c ConsumersPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := c.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractConsumers returns a slice of Consumers contained in a single page of +// results. +func ExtractConsumers(r pagination.Page) ([]Consumer, error) { + var s struct { + Consumers []Consumer `json:"consumers"` + } + err := (r.(ConsumersPage)).ExtractInto(&s) + return s.Consumers, err +} + +// Extract interprets any consumer result as a Consumer. +func (c consumerResult) Extract() (*Consumer, error) { + var s struct { + Consumer *Consumer `json:"consumer"` + } + err := c.ExtractInto(&s) + return s.Consumer, err +} + +// Token contains an OAuth1 token. +type Token struct { + // OAuthToken is the key value for the oauth token that the Identity API returns. + OAuthToken string `q:"oauth_token"` + // OAuthTokenSecret is the secret value associated with the OAuth Token. + OAuthTokenSecret string `q:"oauth_token_secret"` + // OAuthExpiresAt is the date and time when an OAuth token expires. + OAuthExpiresAt *time.Time `q:"-"` +} + +// TokenResult is a struct to handle +// "Content-Type: application/x-www-form-urlencoded" response. +type TokenResult struct { + gophercloud.Result + Body []byte +} + +// Extract interprets any OAuth1 token result as a Token. +func (r TokenResult) Extract() (*Token, error) { + if r.Err != nil { + return nil, r.Err + } + + values, err := url.ParseQuery(string(r.Body)) + if err != nil { + return nil, err + } + + token := &Token{ + OAuthToken: values.Get("oauth_token"), + OAuthTokenSecret: values.Get("oauth_token_secret"), + } + + if v := values.Get("oauth_expires_at"); v != "" { + if t, err := time.Parse(gophercloud.RFC3339Milli, v); err != nil { + return nil, err + } else { + token.OAuthExpiresAt = &t + } + } + + return token, nil +} + +// AuthorizedToken contains an OAuth1 authorized token info. +type AuthorizedToken struct { + // OAuthVerifier is the ID of the token verifier. + OAuthVerifier string `json:"oauth_verifier"` +} + +type AuthorizeTokenResult struct { + gophercloud.Result +} + +// Extract interprets AuthorizeTokenResult result as a AuthorizedToken. +func (r AuthorizeTokenResult) Extract() (*AuthorizedToken, error) { + var s struct { + AuthorizedToken *AuthorizedToken `json:"token"` + } + err := r.ExtractInto(&s) + return s.AuthorizedToken, err +} + +// AccessToken represents an AccessToken response as a struct. +type AccessToken struct { + ID string `json:"id"` + ConsumerID string `json:"consumer_id"` + ProjectID string `json:"project_id"` + AuthorizingUserID string `json:"authorizing_user_id"` + ExpiresAt *time.Time `json:"-"` +} + +func (r *AccessToken) UnmarshalJSON(b []byte) error { + type tmp AccessToken + var s struct { + tmp + ExpiresAt *gophercloud.JSONRFC3339Milli `json:"expires_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = AccessToken(s.tmp) + + if s.ExpiresAt != nil { + t := time.Time(*s.ExpiresAt) + r.ExpiresAt = &t + } + + return nil +} + +type GetAccessTokenResult struct { + gophercloud.Result +} + +// Extract interprets any GetAccessTokenResult result as an AccessToken. +func (r GetAccessTokenResult) Extract() (*AccessToken, error) { + var s struct { + AccessToken *AccessToken `json:"access_token"` + } + err := r.ExtractInto(&s) + return s.AccessToken, err +} + +// RevokeAccessTokenResult is the response from a Delete operation. Call its +// ExtractErr to determine if the request succeeded or failed. +type RevokeAccessTokenResult struct { + gophercloud.ErrResult +} + +// AccessTokensPage is a single page of Access Tokens results. +type AccessTokensPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a an AccessTokensPage contains any results. +func (r AccessTokensPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + accessTokens, err := ExtractAccessTokens(r) + return len(accessTokens) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r AccessTokensPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractAccessTokens returns a slice of AccessTokens contained in a single +// page of results. +func ExtractAccessTokens(r pagination.Page) ([]AccessToken, error) { + var s struct { + AccessTokens []AccessToken `json:"access_tokens"` + } + err := (r.(AccessTokensPage)).ExtractInto(&s) + return s.AccessTokens, err +} + +// AccessTokenRole represents an Access Token Role struct. +type AccessTokenRole struct { + ID string `json:"id"` + Name string `json:"name"` + DomainID string `json:"domain_id"` +} + +// AccessTokenRolesPage is a single page of Access Token roles results. +type AccessTokenRolesPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a an AccessTokensPage contains any results. +func (r AccessTokenRolesPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + accessTokenRoles, err := ExtractAccessTokenRoles(r) + return len(accessTokenRoles) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r AccessTokenRolesPage) NextPageURL() (string, error) { + var s struct { + Links struct { + Next string `json:"next"` + Previous string `json:"previous"` + } `json:"links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return s.Links.Next, err +} + +// ExtractAccessTokenRoles returns a slice of AccessTokenRole contained in a +// single page of results. +func ExtractAccessTokenRoles(r pagination.Page) ([]AccessTokenRole, error) { + var s struct { + AccessTokenRoles []AccessTokenRole `json:"roles"` + } + err := (r.(AccessTokenRolesPage)).ExtractInto(&s) + return s.AccessTokenRoles, err +} + +type GetAccessTokenRoleResult struct { + gophercloud.Result +} + +// Extract interprets any GetAccessTokenRoleResult result as an AccessTokenRole. +func (r GetAccessTokenRoleResult) Extract() (*AccessTokenRole, error) { + var s struct { + AccessTokenRole *AccessTokenRole `json:"role"` + } + err := r.ExtractInto(&s) + return s.AccessTokenRole, err +} + +// OAuth1 is an OAuth1 object, returned in OAuth1 token result. +type OAuth1 struct { + AccessTokenID string `json:"access_token_id"` + ConsumerID string `json:"consumer_id"` +} + +// TokenExt represents an extension of the base token result. +type TokenExt struct { + OAuth1 OAuth1 `json:"OS-OAUTH1"` +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/urls.go new file mode 100644 index 000000000..c8dc02e5d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1/urls.go @@ -0,0 +1,43 @@ +package oauth1 + +import "github.com/gophercloud/gophercloud/v2" + +func consumersURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "consumers") +} + +func consumerURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("OS-OAUTH1", "consumers", id) +} + +func requestTokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "request_token") +} + +func authorizeTokenURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("OS-OAUTH1", "authorize", id) +} + +func createAccessTokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("OS-OAUTH1", "access_token") +} + +func userAccessTokensURL(c *gophercloud.ServiceClient, userID string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens") +} + +func userAccessTokenURL(c *gophercloud.ServiceClient, userID string, id string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id) +} + +func userAccessTokenRolesURL(c *gophercloud.ServiceClient, userID string, id string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles") +} + +func userAccessTokenRoleURL(c *gophercloud.ServiceClient, userID string, id string, roleID string) string { + return c.ServiceURL("users", userID, "OS-OAUTH1", "access_tokens", id, "roles", roleID) +} + +func authURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/doc.go new file mode 100644 index 000000000..c711d0c28 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,107 @@ +/* +Package tokens provides information and interaction with the token API +resource for the OpenStack Identity service. + +For more information, see: +http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3 + +Example to Create a Token From a Username and Password + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + } + + token, err := tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Username, Password, and Domain + + authOptions := tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainID: "default", + } + + token, err := tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + + authOptions = tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainName: "default", + } + + token, err = tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token From a Token + + authOptions := tokens.AuthOptions{ + TokenID: "token_id", + } + + token, err := tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project ID Scope + + scope := tokens.Scope{ + ProjectID: "0fe36e73809d46aeae6705c39077b1b3", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Domain ID Scope + + scope := tokens.Scope{ + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +Example to Create a Token from a Username and Password with Project Name Scope + + scope := tokens.Scope{ + ProjectName: "project_name", + DomainID: "default", + } + + authOptions := tokens.AuthOptions{ + Scope: &scope, + UserID: "username", + Password: "password", + } + + token, err = tokens.Create(context.TODO(), identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } +*/ +package tokens diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/requests.go new file mode 100644 index 000000000..fa8b925d0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,179 @@ +package tokens + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" +) + +// Scope allows a created token to be limited to a specific domain or project. +type Scope struct { + ProjectID string + ProjectName string + DomainID string + DomainName string + System bool + TrustID string +} + +// AuthOptionsBuilder provides the ability for extensions to add additional +// parameters to AuthOptions. Extensions must satisfy all required methods. +type AuthOptionsBuilder interface { + // ToTokenV3CreateMap assembles the Create request body, returning an error + // if parameters are missing or inconsistent. + ToTokenV3CreateMap(map[string]any) (map[string]any, error) + ToTokenV3HeadersMap(map[string]any) (map[string]string, error) + ToTokenV3ScopeMap() (map[string]any, error) + CanReauth() bool +} + +// AuthOptions represents options for authenticating a user. +type AuthOptions struct { + // IdentityEndpoint specifies the HTTP endpoint that is required to work with + // the Identity API of the appropriate version. While it's ultimately needed + // by all of the identity services, it will often be populated by a + // provider-level function. + IdentityEndpoint string `json:"-"` + + // Username is required if using Identity V2 API. Consult with your provider's + // control panel to discover your account's username. In Identity V3, either + // UserID or a combination of Username and DomainID or DomainName are needed. + Username string `json:"username,omitempty"` + UserID string `json:"id,omitempty"` + + Password string `json:"password,omitempty"` + + // Passcode is used in TOTP authentication method + Passcode string `json:"passcode,omitempty"` + + // At most one of DomainID and DomainName must be provided if using Username + // with Identity V3. Otherwise, either are optional. + DomainID string `json:"-"` + DomainName string `json:"name,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud + // to cache your credentials in memory, and to allow Gophercloud to attempt + // to re-authenticate automatically if/when your token expires. If you set + // it to false, it will not cache these settings, but re-authentication will + // not be possible. This setting defaults to false. + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // Authentication through Application Credentials requires supplying name, project and secret + // For project we can use TenantID + ApplicationCredentialID string `json:"-"` + ApplicationCredentialName string `json:"-"` + ApplicationCredentialSecret string `json:"-"` + + Scope Scope `json:"-"` +} + +// ToTokenV3CreateMap builds a request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]any) (map[string]any, error) { + gophercloudAuthOpts := gophercloud.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + Passcode: opts.Passcode, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + ApplicationCredentialID: opts.ApplicationCredentialID, + ApplicationCredentialName: opts.ApplicationCredentialName, + ApplicationCredentialSecret: opts.ApplicationCredentialSecret, + } + + return gophercloudAuthOpts.ToTokenV3CreateMap(scope) +} + +// ToTokenV3ScopeMap builds a scope request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]any, error) { + scope := gophercloud.AuthScope(opts.Scope) + + gophercloudAuthOpts := gophercloud.AuthOptions{ + Scope: &scope, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + } + + return gophercloudAuthOpts.ToTokenV3ScopeMap() +} + +func (opts *AuthOptions) CanReauth() bool { + if opts.Passcode != "" { + // cannot reauth using TOTP passcode + return false + } + + return opts.AllowReauth +} + +// ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v3 tokens package. +func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]any) (map[string]string, error) { + return nil, nil +} + +func subjectTokenHeaders(subjectToken string) map[string]string { + return map[string]string{ + "X-Subject-Token": subjectToken, + } +} + +// Create authenticates and either generates a new token, or changes the Scope +// of an existing token. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts AuthOptionsBuilder) (r CreateResult) { + scope, err := opts.ToTokenV3ScopeMap() + if err != nil { + r.Err = err + return + } + + b, err := opts.ToTokenV3CreateMap(scope) + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(ctx, tokenURL(c), b, &r.Body, &gophercloud.RequestOpts{ + OmitHeaders: []string{"X-Auth-Token"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get validates and retrieves information about another token. +func Get(ctx context.Context, c *gophercloud.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(ctx, tokenURL(c), &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + OkCodes: []int{200, 203}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Validate determines if a specified token is valid or not. +func Validate(ctx context.Context, c *gophercloud.ServiceClient, token string) (bool, error) { + resp, err := c.Head(ctx, tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + OkCodes: []int{200, 204, 404}, + }) + if err != nil { + return false, err + } + + return resp.StatusCode == 200 || resp.StatusCode == 204, nil +} + +// Revoke immediately makes specified token invalid. +func Revoke(ctx context.Context, c *gophercloud.ServiceClient, token string) (r RevokeResult) { + resp, err := c.Delete(ctx, tokenURL(c), &gophercloud.RequestOpts{ + MoreHeaders: subjectTokenHeaders(token), + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/results.go new file mode 100644 index 000000000..bd61a9a67 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/results.go @@ -0,0 +1,215 @@ +package tokens + +import ( + "time" + + "github.com/gophercloud/gophercloud/v2" +) + +// Endpoint represents a single API endpoint offered by a service. +// It matches either a public, internal or admin URL. +// If supported, it contains a region specifier, again if provided. +// The significance of the Region field will depend upon your provider. +type Endpoint struct { + ID string `json:"id"` + Region string `json:"region"` + RegionID string `json:"region_id"` + Interface string `json:"interface"` + URL string `json:"url"` +} + +// CatalogEntry provides a type-safe interface to an Identity API V3 service +// catalog listing. Each class of service, such as cloud DNS or block storage +// services, could have multiple CatalogEntry representing it (one by interface +// type, e.g public, admin or internal). +// +// Note: when looking for the desired service, try, whenever possible, to key +// off the type field. Otherwise, you'll tie the representation of the service +// to a specific provider. +type CatalogEntry struct { + // Service ID + ID string `json:"id"` + + // Name will contain the provider-specified name for the service. + Name string `json:"name"` + + // Type will contain a type string if OpenStack defines a type for the + // service. Otherwise, for provider-specific services, the provider may + // assign their own type strings. + Type string `json:"type"` + + // Endpoints will let the caller iterate over all the different endpoints that + // may exist for the service. + Endpoints []Endpoint `json:"endpoints"` +} + +// ServiceCatalog provides a view into the service catalog from a previous, +// successful authentication. +type ServiceCatalog struct { + Entries []CatalogEntry `json:"catalog"` +} + +// Domain provides information about the domain to which this token grants +// access. +type Domain struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// User represents a user resource that exists in the Identity Service. +type User struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +// Role provides information about roles to which User is authorized. +type Role struct { + ID string `json:"id"` + Name string `json:"name"` +} + +// Project provides information about project to which User is authorized. +type Project struct { + Domain Domain `json:"domain"` + ID string `json:"id"` + Name string `json:"name"` +} + +type TrustUser struct { + ID string `json:"id"` +} + +// Trust provides information about trust with which User is authorized. +type Trust struct { + ID string `json:"id"` + Impersonation bool `json:"impersonation"` + TrusteeUserID TrustUser `json:"trustee_user"` + TrustorUserID TrustUser `json:"trustor_user"` +} + +// commonResult is the response from a request. A commonResult has various +// methods which can be used to extract different details about the result. +type commonResult struct { + gophercloud.Result +} + +// Extract is a shortcut for ExtractToken. +// This function is deprecated and still present for backward compatibility. +func (r commonResult) Extract() (*Token, error) { + return r.ExtractToken() +} + +// ExtractToken interprets a commonResult as a Token. +func (r commonResult) ExtractToken() (*Token, error) { + var s Token + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + // Parse the token itself from the stored headers. + s.ID = r.Header.Get("X-Subject-Token") + + return &s, err +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r CreateResult) ExtractTokenID() (string, error) { + return r.Header.Get("X-Subject-Token"), r.Err +} + +// ExtractTokenID implements the gophercloud.AuthResult interface. The returned +// string is the same as the ID field of the Token struct returned from +// ExtractToken(). +func (r GetResult) ExtractTokenID() (string, error) { + return r.Header.Get("X-Subject-Token"), r.Err +} + +// ExtractServiceCatalog returns the ServiceCatalog that was generated along +// with the user's Token. +func (r commonResult) ExtractServiceCatalog() (*ServiceCatalog, error) { + var s ServiceCatalog + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractUser returns the User that is the owner of the Token. +func (r commonResult) ExtractUser() (*User, error) { + var s struct { + User *User `json:"user"` + } + err := r.ExtractInto(&s) + return s.User, err +} + +// ExtractRoles returns Roles to which User is authorized. +func (r commonResult) ExtractRoles() ([]Role, error) { + var s struct { + Roles []Role `json:"roles"` + } + err := r.ExtractInto(&s) + return s.Roles, err +} + +// ExtractProject returns Project to which User is authorized. +func (r commonResult) ExtractProject() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} + +// ExtractDomain returns Domain to which User is authorized. +func (r commonResult) ExtractDomain() (*Domain, error) { + var s struct { + Domain *Domain `json:"domain"` + } + err := r.ExtractInto(&s) + return s.Domain, err +} + +// ExtractTrust returns Trust to which User is authorized. +func (r commonResult) ExtractTrust() (*Trust, error) { + var s struct { + Trust *Trust `json:"OS-TRUST:trust"` + } + err := r.ExtractInto(&s) + return s.Trust, err +} + +// CreateResult is the response from a Create request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type CreateResult struct { + commonResult +} + +// GetResult is the response from a Get request. Use ExtractToken() +// to interpret it as a Token, or ExtractServiceCatalog() to interpret it +// as a service catalog. +type GetResult struct { + commonResult +} + +// RevokeResult is response from a Revoke request. +type RevokeResult struct { + commonResult +} + +// Token is a string that grants a user access to a controlled set of services +// in an OpenStack provider. Each Token is valid for a set length of time. +type Token struct { + // ID is the issued token. + ID string `json:"id"` + + // ExpiresAt is the timestamp at which this token will no longer be accepted. + ExpiresAt time.Time `json:"expires_at"` +} + +func (r commonResult) ExtractInto(v any) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/urls.go new file mode 100644 index 000000000..2218c107f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/gophercloud/gophercloud/v2" + +func tokenURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/doc.go new file mode 100644 index 000000000..f99cf743f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/doc.go @@ -0,0 +1,51 @@ +/* +Package imagedata enables management of image data. + +Example to Upload Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + imageData, err := os.Open("/path/to/image/file") + if err != nil { + panic(err) + } + defer imageData.Close() + + err = imagedata.Upload(context.TODO(), imageClient, imageID, imageData).ExtractErr() + if err != nil { + panic(err) + } + +Example to Stage Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + imageData, err := os.Open("/path/to/image/file") + if err != nil { + panic(err) + } + defer imageData.Close() + + err = imagedata.Stage(context.TODO(), imageClient, imageID, imageData).ExtractErr() + if err != nil { + panic(err) + } + +Example to Download Image Data + + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + image, err := imagedata.Download(context.TODO(), imageClient, imageID).Extract() + if err != nil { + panic(err) + } + + // close the reader, when reading has finished + defer image.Close() + + imageData, err := io.ReadAll(image) + if err != nil { + panic(err) + } +*/ +package imagedata diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/requests.go new file mode 100644 index 000000000..fd0f3e7ff --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/requests.go @@ -0,0 +1,39 @@ +package imagedata + +import ( + "context" + "io" + + "github.com/gophercloud/gophercloud/v2" +) + +// Upload uploads an image file. +func Upload(ctx context.Context, client *gophercloud.ServiceClient, id string, data io.Reader) (r UploadResult) { + resp, err := client.Put(ctx, uploadURL(client, id), data, nil, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"}, + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Stage performs PUT call on the existing image object in the Image service with +// the provided file. +// Existing image object must be in the "queued" status. +func Stage(ctx context.Context, client *gophercloud.ServiceClient, id string, data io.Reader) (r StageResult) { + resp, err := client.Put(ctx, stageURL(client, id), data, nil, &gophercloud.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/octet-stream"}, + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Download retrieves an image. +func Download(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DownloadResult) { + resp, err := client.Get(ctx, downloadURL(client, id), nil, &gophercloud.RequestOpts{ + KeepResponseBody: true, + }) + r.Body, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/results.go new file mode 100644 index 000000000..27f41b7df --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/results.go @@ -0,0 +1,34 @@ +package imagedata + +import ( + "io" + + "github.com/gophercloud/gophercloud/v2" +) + +// UploadResult is the result of an upload image operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type UploadResult struct { + gophercloud.ErrResult +} + +// StageResult is the result of a stage image operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type StageResult struct { + gophercloud.ErrResult +} + +// DownloadResult is the result of a download image operation. Call its Extract +// method to gain access to the image data. +type DownloadResult struct { + gophercloud.Result + Body io.ReadCloser +} + +// Extract builds images model from io.Reader +func (r DownloadResult) Extract() (io.ReadCloser, error) { + if r.Err != nil { + return nil, r.Err + } + return r.Body, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/urls.go new file mode 100644 index 000000000..21b7cceb3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata/urls.go @@ -0,0 +1,23 @@ +package imagedata + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "images" + uploadPath = "file" + stagePath = "stage" +) + +// `imageDataURL(c,i)` is the URL for the binary image data for the +// image identified by ID `i` in the service `c`. +func uploadURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL(rootPath, imageID, uploadPath) +} + +func stageURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL(rootPath, imageID, stagePath) +} + +func downloadURL(c *gophercloud.ServiceClient, imageID string) string { + return uploadURL(c, imageID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/doc.go new file mode 100644 index 000000000..8a36af94b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/doc.go @@ -0,0 +1,27 @@ +/* +Package imageimport enables management of images import and retrieval of the +Image service Import API information. + +Example to Get an information about the Import API + + importInfo, err := imageimport.Get(context.TODO(), imagesClient).Extract() + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", importInfo) + +Example to Create a new image import + + createOpts := imageimport.CreateOpts{ + Name: imageimport.WebDownloadMethod, + URI: "http://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img", + } + imageID := "da3b75d9-3f4a-40e7-8a2c-bfab23927dea" + + err := imageimport.Create(context.TODO(), imagesClient, imageID, createOpts).ExtractErr() + if err != nil { + panic(err) + } +*/ +package imageimport diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/requests.go new file mode 100644 index 000000000..186e5bab3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/requests.go @@ -0,0 +1,59 @@ +package imageimport + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" +) + +// ImportMethod represents valid Import API method. +type ImportMethod string + +const ( + // GlanceDirectMethod represents glance-direct Import API method. + GlanceDirectMethod ImportMethod = "glance-direct" + + // WebDownloadMethod represents web-download Import API method. + WebDownloadMethod ImportMethod = "web-download" +) + +// Get retrieves Import API information data. +func Get(ctx context.Context, c *gophercloud.ServiceClient) (r GetResult) { + resp, err := c.Get(ctx, infoURL(c), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows to add additional parameters to the Create request. +type CreateOptsBuilder interface { + ToImportCreateMap() (map[string]any, error) +} + +// CreateOpts specifies parameters of a new image import. +type CreateOpts struct { + Name ImportMethod `json:"name"` + URI string `json:"uri"` +} + +// ToImportCreateMap constructs a request body from CreateOpts. +func (opts CreateOpts) ToImportCreateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + return map[string]any{"method": b}, nil +} + +// Create requests the creation of a new image import on the server. +func Create(ctx context.Context, client *gophercloud.ServiceClient, imageID string, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImportCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := client.Post(ctx, importURL(client, imageID), b, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/results.go new file mode 100644 index 000000000..d5f1dbab8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/results.go @@ -0,0 +1,38 @@ +package imageimport + +import "github.com/gophercloud/gophercloud/v2" + +type commonResult struct { + gophercloud.Result +} + +// GetResult represents the result of a get operation. Call its Extract method +// to interpret it as ImportInfo. +type GetResult struct { + commonResult +} + +// CreateResult is the result of import Create operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type CreateResult struct { + gophercloud.ErrResult +} + +// ImportInfo represents information data for the Import API. +type ImportInfo struct { + ImportMethods ImportMethods `json:"import-methods"` +} + +// ImportMethods contains information about available Import API methods. +type ImportMethods struct { + Description string `json:"description"` + Type string `json:"type"` + Value []string `json:"value"` +} + +// Extract is a function that accepts a result and extracts ImportInfo. +func (r commonResult) Extract() (*ImportInfo, error) { + var s *ImportInfo + err := r.ExtractInto(&s) + return s, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/urls.go new file mode 100644 index 000000000..797b00549 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport/urls.go @@ -0,0 +1,17 @@ +package imageimport + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "images" + infoPath = "info" + resourcePath = "import" +) + +func infoURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(infoPath, resourcePath) +} + +func importURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL(rootPath, imageID, resourcePath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/doc.go new file mode 100644 index 000000000..bf1a6cd5b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/doc.go @@ -0,0 +1,60 @@ +/* +Package images enables management and retrieval of images from the OpenStack +Image Service. + +Example to List Images + + images.ListOpts{ + Owner: "a7509e1ae65945fda83f3e52c6296017", + } + + allPages, err := images.List(imagesClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allImages, err := images.ExtractImages(allPages) + if err != nil { + panic(err) + } + + for _, image := range allImages { + fmt.Printf("%+v\n", image) + } + +Example to Create an Image + + createOpts := images.CreateOpts{ + Name: "image_name", + Visibility: images.ImageVisibilityPrivate, + } + + image, err := images.Create(context.TODO(), imageClient, createOpts) + if err != nil { + panic(err) + } + +Example to Update an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + + updateOpts := images.UpdateOpts{ + images.ReplaceImageName{ + NewName: "new_name", + }, + } + + image, err := images.Update(context.TODO(), imageClient, imageID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete an Image + + imageID := "1bea47ed-f6a9-463b-b423-14b9cca9ad27" + err := images.Delete(context.TODO(), imageClient, imageID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package images diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/requests.go new file mode 100644 index 000000000..a5f61dd61 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/requests.go @@ -0,0 +1,419 @@ +package images + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToImageListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the server attributes you want to see returned. Marker and Limit are used +// for pagination. +// +// http://developer.openstack.org/api-ref-image-v2.html +type ListOpts struct { + // ID is the ID of the image. + // Multiple IDs can be specified by constructing a string + // such as "in:uuid1,uuid2,uuid3". + ID string `q:"id"` + + // Integer value for the limit of values to return. + Limit int `q:"limit"` + + // UUID of the server at which you want to set a marker. + Marker string `q:"marker"` + + // Name filters on the name of the image. + // Multiple names can be specified by constructing a string + // such as "in:name1,name2,name3". + Name string `q:"name"` + + // Visibility filters on the visibility of the image. + Visibility ImageVisibility `q:"visibility"` + + // Hidden filters on the hidden status of the image. + Hidden bool `q:"os_hidden"` + + // MemberStatus filters on the member status of the image. + MemberStatus ImageMemberStatus `q:"member_status"` + + // Owner filters on the project ID of the image. + Owner string `q:"owner"` + + // Status filters on the status of the image. + // Multiple statuses can be specified by constructing a string + // such as "in:saving,queued". + Status ImageStatus `q:"status"` + + // SizeMin filters on the size_min image property. + SizeMin int64 `q:"size_min"` + + // SizeMax filters on the size_max image property. + SizeMax int64 `q:"size_max"` + + // Sort sorts the results using the new style of sorting. See the OpenStack + // Image API reference for the exact syntax. + // + // Sort cannot be used with the classic sort options (sort_key and sort_dir). + Sort string `q:"sort"` + + // SortKey will sort the results based on a specified image property. + SortKey string `q:"sort_key"` + + // SortDir will sort the list results either ascending or decending. + SortDir string `q:"sort_dir"` + + // Tags filters on specific image tags. + Tags []string `q:"tag"` + + // CreatedAtQuery filters images based on their creation date. + CreatedAtQuery *ImageDateQuery + + // UpdatedAtQuery filters images based on their updated date. + UpdatedAtQuery *ImageDateQuery + + // ContainerFormat filters images based on the container_format. + // Multiple container formats can be specified by constructing a + // string such as "in:bare,ami". + ContainerFormat string `q:"container_format"` + + // DiskFormat filters images based on the disk_format. + // Multiple disk formats can be specified by constructing a string + // such as "in:qcow2,iso". + DiskFormat string `q:"disk_format"` +} + +// ToImageListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToImageListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + params := q.Query() + + if opts.CreatedAtQuery != nil { + createdAt := opts.CreatedAtQuery.Date.Format(time.RFC3339) + if v := opts.CreatedAtQuery.Filter; v != "" { + createdAt = fmt.Sprintf("%s:%s", v, createdAt) + } + + params.Add("created_at", createdAt) + } + + if opts.UpdatedAtQuery != nil { + updatedAt := opts.UpdatedAtQuery.Date.Format(time.RFC3339) + if v := opts.UpdatedAtQuery.Filter; v != "" { + updatedAt = fmt.Sprintf("%s:%s", v, updatedAt) + } + + params.Add("updated_at", updatedAt) + } + + q = &url.URL{RawQuery: params.Encode()} + + return q.String(), err +} + +// List implements image list request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToImageListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + imagePage := ImagePage{ + serviceURL: c.ServiceURL(), + LinkedPageBase: pagination.LinkedPageBase{PageResult: r}, + } + + return imagePage + }) +} + +// CreateOptsBuilder allows extensions to add parameters to the Create request. +type CreateOptsBuilder interface { + // Returns value that can be passed to json.Marshal + ToImageCreateMap() (map[string]any, error) +} + +// CreateOpts represents options used to create an image. +type CreateOpts struct { + // Name is the name of the new image. + Name string `json:"name" required:"true"` + + // Id is the the image ID. + ID string `json:"id,omitempty"` + + // Visibility defines who can see/use the image. + Visibility *ImageVisibility `json:"visibility,omitempty"` + + // Hidden is whether the image is listed in default image list or not. + Hidden *bool `json:"os_hidden,omitempty"` + + // Tags is a set of image tags. + Tags []string `json:"tags,omitempty"` + + // ContainerFormat is the format of the + // container. Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format,omitempty"` + + // DiskFormat is the format of the disk. If set, + // valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format,omitempty"` + + // MinDisk is the amount of disk space in + // GB that is required to boot the image. + MinDisk int `json:"min_disk,omitempty"` + + // MinRAM is the amount of RAM in MB that + // is required to boot the image. + MinRAM int `json:"min_ram,omitempty"` + + // protected is whether the image is not deletable. + Protected *bool `json:"protected,omitempty"` + + // properties is a set of properties, if any, that + // are associated with the image. + Properties map[string]string `json:"-"` +} + +// ToImageCreateMap assembles a request body based on the contents of +// a CreateOpts. +func (opts CreateOpts) ToImageCreateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if opts.Properties != nil { + for k, v := range opts.Properties { + b[k] = v + } + } + return b, nil +} + +// Create implements create image request. +func Create(ctx context.Context, client *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToImageCreateMap() + if err != nil { + r.Err = err + return r + } + resp, err := client.Post(ctx, createURL(client), b, &r.Body, &gophercloud.RequestOpts{OkCodes: []int{201}}) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete implements image delete request. +func Delete(ctx context.Context, client *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := client.Delete(ctx, deleteURL(client, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get implements image get request. +func Get(ctx context.Context, client *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := client.Get(ctx, getURL(client, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Update implements image updated request. +func Update(ctx context.Context, client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToImageUpdateMap() + if err != nil { + r.Err = err + return r + } + resp, err := client.Patch(ctx, updateURL(client, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: map[string]string{"Content-Type": "application/openstack-images-v2.1-json-patch"}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + // returns value implementing json.Marshaler which when marshaled matches + // the patch schema: + // http://specs.openstack.org/openstack/glance-specs/specs/api/v2/http-patch-image-api-v2.html + ToImageUpdateMap() ([]any, error) +} + +// UpdateOpts implements UpdateOpts +type UpdateOpts []Patch + +// ToImageUpdateMap assembles a request body based on the contents of +// UpdateOpts. +func (opts UpdateOpts) ToImageUpdateMap() ([]any, error) { + m := make([]any, len(opts)) + for i, patch := range opts { + patchJSON := patch.ToImagePatchMap() + m[i] = patchJSON + } + return m, nil +} + +// Patch represents a single update to an existing image. Multiple updates +// to an image can be submitted at the same time. +type Patch interface { + ToImagePatchMap() map[string]any +} + +// UpdateVisibility represents an updated visibility property request. +type UpdateVisibility struct { + Visibility ImageVisibility +} + +// ToImagePatchMap assembles a request body based on UpdateVisibility. +func (r UpdateVisibility) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/visibility", + "value": r.Visibility, + } +} + +// ReplaceImageHidden represents an updated os_hidden property request. +type ReplaceImageHidden struct { + NewHidden bool +} + +// ToImagePatchMap assembles a request body based on ReplaceImageHidden. +func (r ReplaceImageHidden) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/os_hidden", + "value": r.NewHidden, + } +} + +// ReplaceImageName represents an updated image_name property request. +type ReplaceImageName struct { + NewName string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageName. +func (r ReplaceImageName) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/name", + "value": r.NewName, + } +} + +// ReplaceImageChecksum represents an updated checksum property request. +type ReplaceImageChecksum struct { + Checksum string +} + +// ReplaceImageChecksum assembles a request body based on ReplaceImageChecksum. +func (r ReplaceImageChecksum) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/checksum", + "value": r.Checksum, + } +} + +// ReplaceImageTags represents an updated tags property request. +type ReplaceImageTags struct { + NewTags []string +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageTags) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/tags", + "value": r.NewTags, + } +} + +// ReplaceImageMinDisk represents an updated min_disk property request. +type ReplaceImageMinDisk struct { + NewMinDisk int +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageMinDisk) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/min_disk", + "value": r.NewMinDisk, + } +} + +// ReplaceImageMinRam represents an updated min_ram property request. +type ReplaceImageMinRam struct { + NewMinRam int +} + +// ToImagePatchMap assembles a request body based on ReplaceImageTags. +func (r ReplaceImageMinRam) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/min_ram", + "value": r.NewMinRam, + } +} + +// ReplaceImageProtected represents an updated protected property request. +type ReplaceImageProtected struct { + NewProtected bool +} + +// ToImagePatchMap assembles a request body based on ReplaceImageProtected +func (r ReplaceImageProtected) ToImagePatchMap() map[string]any { + return map[string]any{ + "op": "replace", + "path": "/protected", + "value": r.NewProtected, + } +} + +// UpdateOp represents a valid update operation. +type UpdateOp string + +const ( + AddOp UpdateOp = "add" + ReplaceOp UpdateOp = "replace" + RemoveOp UpdateOp = "remove" +) + +// UpdateImageProperty represents an update property request. +type UpdateImageProperty struct { + Op UpdateOp + Name string + Value string +} + +// ToImagePatchMap assembles a request body based on UpdateImageProperty. +func (r UpdateImageProperty) ToImagePatchMap() map[string]any { + updateMap := map[string]any{ + "op": r.Op, + "path": fmt.Sprintf("/%s", r.Name), + } + + if r.Op != RemoveOp { + updateMap["value"] = r.Value + } + + return updateMap +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/results.go new file mode 100644 index 000000000..6652f0e79 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/results.go @@ -0,0 +1,246 @@ +package images + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Image represents an image found in the OpenStack Image service. +type Image struct { + // ID is the image UUID. + ID string `json:"id"` + + // Name is the human-readable display name for the image. + Name string `json:"name"` + + // Status is the image status. It can be "queued" or "active" + // See image/v2/images/type.go + Status ImageStatus `json:"status"` + + // Tags is a list of image tags. Tags are arbitrarily defined strings + // attached to an image. + Tags []string `json:"tags"` + + // ContainerFormat is the format of the container. + // Valid values are ami, ari, aki, bare, and ovf. + ContainerFormat string `json:"container_format"` + + // DiskFormat is the format of the disk. + // If set, valid values are ami, ari, aki, vhd, vmdk, raw, qcow2, vdi, + // and iso. + DiskFormat string `json:"disk_format"` + + // MinDiskGigabytes is the amount of disk space in GB that is required to + // boot the image. + MinDiskGigabytes int `json:"min_disk"` + + // MinRAMMegabytes [optional] is the amount of RAM in MB that is required to + // boot the image. + MinRAMMegabytes int `json:"min_ram"` + + // Owner is the tenant ID the image belongs to. + Owner string `json:"owner"` + + // Protected is whether the image is deletable or not. + Protected bool `json:"protected"` + + // Visibility defines who can see/use the image. + Visibility ImageVisibility `json:"visibility"` + + // Hidden is whether the image is listed in default image list or not. + Hidden bool `json:"os_hidden"` + + // Checksum is the checksum of the data that's associated with the image. + Checksum string `json:"checksum"` + + // SizeBytes is the size of the data that's associated with the image. + SizeBytes int64 `json:"-"` + + // Metadata is a set of metadata associated with the image. + // Image metadata allow for meaningfully define the image properties + // and tags. + // See http://docs.openstack.org/developer/glance/metadefs-concepts.html. + Metadata map[string]string `json:"metadata"` + + // Properties is a set of key-value pairs, if any, that are associated with + // the image. + Properties map[string]any + + // CreatedAt is the date when the image has been created. + CreatedAt time.Time `json:"created_at"` + + // UpdatedAt is the date when the last change has been made to the image or + // its properties. + UpdatedAt time.Time `json:"updated_at"` + + // File is the trailing path after the glance endpoint that represent the + // location of the image or the path to retrieve it. + File string `json:"file"` + + // Schema is the path to the JSON-schema that represent the image or image + // entity. + Schema string `json:"schema"` + + // VirtualSize is the virtual size of the image + VirtualSize int64 `json:"virtual_size"` + + // OpenStackImageImportMethods is a slice listing the types of import + // methods available in the cloud. + OpenStackImageImportMethods []string `json:"-"` + // OpenStackImageStoreIDs is a slice listing the store IDs available in + // the cloud. + OpenStackImageStoreIDs []string `json:"-"` +} + +func (r *Image) UnmarshalJSON(b []byte) error { + type tmp Image + var s struct { + tmp + SizeBytes any `json:"size"` + OpenStackImageImportMethods string `json:"openstack-image-import-methods"` + OpenStackImageStoreIDs string `json:"openstack-image-store-ids"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Image(s.tmp) + + switch t := s.SizeBytes.(type) { + case nil: + r.SizeBytes = 0 + case float32: + r.SizeBytes = int64(t) + case float64: + r.SizeBytes = int64(t) + default: + return fmt.Errorf("Unknown type for SizeBytes: %v (value: %v)", reflect.TypeOf(t), t) + } + + // Bundle all other fields into Properties + var result any + err = json.Unmarshal(b, &result) + if err != nil { + return err + } + if resultMap, ok := result.(map[string]any); ok { + delete(resultMap, "self") + delete(resultMap, "size") + delete(resultMap, "openstack-image-import-methods") + delete(resultMap, "openstack-image-store-ids") + r.Properties = gophercloud.RemainingKeys(Image{}, resultMap) + } + + if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageImportMethods), splitFunc); len(v) > 0 { + r.OpenStackImageImportMethods = v + } + if v := strings.FieldsFunc(strings.TrimSpace(s.OpenStackImageStoreIDs), splitFunc); len(v) > 0 { + r.OpenStackImageStoreIDs = v + } + + return err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract interprets any commonResult as an Image. +func (r commonResult) Extract() (*Image, error) { + var s *Image + if v, ok := r.Body.(map[string]any); ok { + for k, h := range r.Header { + if strings.ToLower(k) == "openstack-image-import-methods" { + for _, s := range h { + v["openstack-image-import-methods"] = s + } + } + if strings.ToLower(k) == "openstack-image-store-ids" { + for _, s := range h { + v["openstack-image-store-ids"] = s + } + } + } + } + err := r.ExtractInto(&s) + return s, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret it as an Image. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret it as an Image. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret it as an Image. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to interpret it as an Image. +type DeleteResult struct { + gophercloud.ErrResult +} + +// ImagePage represents the results of a List request. +type ImagePage struct { + serviceURL string + pagination.LinkedPageBase +} + +// IsEmpty returns true if an ImagePage contains no Images results. +func (r ImagePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + images, err := ExtractImages(r) + return len(images) == 0, err +} + +// NextPageURL uses the response's embedded link reference to navigate to +// the next page of results. +func (r ImagePage) NextPageURL() (string, error) { + var s struct { + Next string `json:"next"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + if s.Next == "" { + return "", nil + } + + return nextPageURL(r.serviceURL, s.Next) +} + +// ExtractImages interprets the results of a single page from a List() call, +// producing a slice of Image entities. +func ExtractImages(r pagination.Page) ([]Image, error) { + var s struct { + Images []Image `json:"images"` + } + err := (r.(ImagePage)).ExtractInto(&s) + return s.Images, err +} + +// splitFunc is a helper function used to avoid a slice of empty strings. +func splitFunc(c rune) bool { + return c == ',' +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/types.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/types.go new file mode 100644 index 000000000..eedc13a33 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/types.go @@ -0,0 +1,112 @@ +package images + +import ( + "time" +) + +// ImageStatus image statuses +// http://docs.openstack.org/developer/glance/statuses.html +type ImageStatus string + +const ( + // ImageStatusQueued is a status for an image which identifier has + // been reserved for an image in the image registry. + ImageStatusQueued ImageStatus = "queued" + + // ImageStatusSaving denotes that an image's raw data is currently being + // uploaded to Glance + ImageStatusSaving ImageStatus = "saving" + + // ImageStatusUploading denotes that an image's raw data is currently being + // uploaded to Glance through the upload process + ImageStatusUploading ImageStatus = "uploading" + + // ImageStatusActive denotes an image that is fully available in Glance. + ImageStatusActive ImageStatus = "active" + + // ImageStatusKilled denotes that an error occurred during the uploading + // of an image’s data, and that the image is not readable. + ImageStatusKilled ImageStatus = "killed" + + // ImageStatusDeleted is used for an image that is no longer available to use. + // The image information is retained in the image registry. + ImageStatusDeleted ImageStatus = "deleted" + + // ImageStatusPendingDelete is similar to Delete, but the image is not yet + // deleted. + ImageStatusPendingDelete ImageStatus = "pending_delete" + + // ImageStatusDeactivated denotes that access to image data is not allowed to + // any non-admin user. + ImageStatusDeactivated ImageStatus = "deactivated" + + // ImageStatusImporting denotes that an import call has been made but that + // the image is not yet ready for use. + ImageStatusImporting ImageStatus = "importing" +) + +// ImageVisibility denotes an image that is fully available in Glance. +// This occurs when the image data is uploaded, or the image size is explicitly +// set to zero on creation. +// According to design +// https://wiki.openstack.org/wiki/Glance-v2-community-image-visibility-design +type ImageVisibility string + +const ( + // ImageVisibilityPublic all users + ImageVisibilityPublic ImageVisibility = "public" + + // ImageVisibilityPrivate users with tenantId == tenantId(owner) + ImageVisibilityPrivate ImageVisibility = "private" + + // ImageVisibilityShared images are visible to: + // - users with tenantId == tenantId(owner) + // - users with tenantId in the member-list of the image + // - users with tenantId in the member-list with member_status == 'accepted' + ImageVisibilityShared ImageVisibility = "shared" + + // ImageVisibilityCommunity images: + // - all users can see and boot it + // - users with tenantId in the member-list of the image with + // member_status == 'accepted' have this image in their default image-list. + ImageVisibilityCommunity ImageVisibility = "community" +) + +// MemberStatus is a status for adding a new member (tenant) to an image +// member list. +type ImageMemberStatus string + +const ( + // ImageMemberStatusAccepted is the status for an accepted image member. + ImageMemberStatusAccepted ImageMemberStatus = "accepted" + + // ImageMemberStatusPending shows that the member addition is pending + ImageMemberStatusPending ImageMemberStatus = "pending" + + // ImageMemberStatusAccepted is the status for a rejected image member + ImageMemberStatusRejected ImageMemberStatus = "rejected" + + // ImageMemberStatusAll + ImageMemberStatusAll ImageMemberStatus = "all" +) + +// ImageDateFilter represents a valid filter to use for filtering +// images by their date during a List. +type ImageDateFilter string + +const ( + FilterGT ImageDateFilter = "gt" + FilterGTE ImageDateFilter = "gte" + FilterLT ImageDateFilter = "lt" + FilterLTE ImageDateFilter = "lte" + FilterNEQ ImageDateFilter = "neq" + FilterEQ ImageDateFilter = "eq" +) + +// ImageDateQuery represents a date field to be used for listing images. +// If no filter is specified, the query will act as though FilterEQ was +// set. +type ImageDateQuery struct { + Date time.Time + Filter ImageDateFilter +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/urls.go new file mode 100644 index 000000000..c3007a612 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/image/v2/images/urls.go @@ -0,0 +1,65 @@ +package images + +import ( + "net/url" + "strings" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/utils" +) + +// `listURL` is a pure function. `listURL(c)` is a URL for which a GET +// request will respond with a list of images in the service `c`. +func listURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +func createURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("images") +} + +// `imageURL(c,i)` is the URL for the image identified by ID `i` in +// the service `c`. +func imageURL(c *gophercloud.ServiceClient, imageID string) string { + return c.ServiceURL("images", imageID) +} + +// `getURL(c,i)` is a URL for which a GET request will respond with +// information about the image identified by ID `i` in the service +// `c`. +func getURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func updateURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +func deleteURL(c *gophercloud.ServiceClient, imageID string) string { + return imageURL(c, imageID) +} + +// builds next page full url based on current url +func nextPageURL(serviceURL, requestedNext string) (string, error) { + base, err := utils.BaseEndpoint(serviceURL) + if err != nil { + return "", err + } + + requestedNextURL, err := url.Parse(requestedNext) + if err != nil { + return "", err + } + + base = gophercloud.NormalizeURL(base) + nextPath := base + strings.TrimPrefix(requestedNextURL.Path, "/") + + nextURL, err := url.Parse(nextPath) + if err != nil { + return "", err + } + + nextURL.RawQuery = requestedNextURL.RawQuery + + return nextURL.String(), nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/doc.go new file mode 100644 index 000000000..d65e3a6dd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/doc.go @@ -0,0 +1,22 @@ +/* +Package apiversions provides information and interaction with the different +API versions for the OpenStack Load Balancer service. This functionality is not +restricted to this particular version. + +Example to List API Versions + + allPages, err := apiversions.List(loadbalancerClient).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allVersions, err := apiversions.ExtractAPIVersions(allPages) + if err != nil { + panic(err) + } + + for _, version := range allVersions { + fmt.Printf("%+v\n", version) + } +*/ +package apiversions diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/requests.go new file mode 100644 index 000000000..1a08dc488 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/requests.go @@ -0,0 +1,13 @@ +package apiversions + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// List lists all the load balancer API versions available to end-users. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return pagination.NewPager(c, listURL(c), func(r pagination.PageResult) pagination.Page { + return APIVersionPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/results.go new file mode 100644 index 000000000..11780a2a4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/results.go @@ -0,0 +1,36 @@ +package apiversions + +import "github.com/gophercloud/gophercloud/v2/pagination" + +// APIVersion represents an API version for load balancer. It contains +// the status of the API, and its unique ID. +type APIVersion struct { + Status string `json:"status"` + ID string `json:"id"` +} + +// APIVersionPage is the page returned by a pager when traversing over a +// collection of API versions. +type APIVersionPage struct { + pagination.SinglePageBase +} + +// IsEmpty checks whether an APIVersionPage struct is empty. +func (r APIVersionPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractAPIVersions(r) + return len(is) == 0, err +} + +// ExtractAPIVersions takes a collection page, extracts all of the elements, +// and returns them a slice of APIVersion structs. It is effectively a cast. +func ExtractAPIVersions(r pagination.Page) ([]APIVersion, error) { + var s struct { + Versions []APIVersion `json:"versions"` + } + err := (r.(APIVersionPage)).ExtractInto(&s) + return s.Versions, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/urls.go new file mode 100644 index 000000000..deaf71765 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions/urls.go @@ -0,0 +1,14 @@ +package apiversions + +import ( + "strings" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/utils" +) + +func listURL(c *gophercloud.ServiceClient) string { + baseEndpoint, _ := utils.BaseEndpoint(c.Endpoint) + endpoint := strings.TrimRight(baseEndpoint, "/") + "/" + return endpoint +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/doc.go new file mode 100644 index 000000000..34139cae4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/doc.go @@ -0,0 +1,58 @@ +/* +Package flavors provides information and interaction with Flavors +for the OpenStack Load-balancing service. + +Example to List Flavors + + listOpts := flavors.ListOpts{} + + allPages, err := flavors.List(octaviaClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allFlavors, err := flavors.ExtractFlavors(allPages) + if err != nil { + panic(err) + } + + for _, flavor := range allFlavors { + fmt.Printf("%+v\n", flavor) + } + +Example to Create a Flavor + + createOpts := flavors.CreateOpts{ + Name: "Flavor name", + Description: "My flavor description", + Enable: true, + FlavorProfileId: "9daa2768-74e7-4d13-bf5d-1b8e0dc239e1", + } + + flavor, err := flavors.Create(context.TODO(), octaviaClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Flavor + + flavorID := "d67d56a6-4a86-4688-a282-f46444705c64" + + updateOpts := flavors.UpdateOpts{ + Name: "New name", + } + + flavor, err := flavors.Update(context.TODO(), octaviaClient, flavorID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Flavor + + flavorID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := flavors.Delete(context.TODO(), octaviaClient, flavorID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package flavors diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/requests.go new file mode 100644 index 000000000..67196a520 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/requests.go @@ -0,0 +1,149 @@ +package flavors + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFlavorListQuery() (string, error) +} + +// ListOpts allows to manage the output of the request. +type ListOpts struct { + // The name of the flavor to filter by. + Name string `q:"name"` + // The flavor profile id to filter by. + FlavorProfileID string `q:"flavor_profile_id"` + // The enabled status of the flavor to filter by. + Enabled *bool `q:"enabled"` + // The fields that you want the server to return + Fields []string `q:"fields"` +} + +// ToFlavorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFlavorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// Flavor. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToFlavorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return FlavorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToFlavorCreateMap() (map[string]any, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name" required:"true"` + + // Human-readable description for the Flavor. + Description string `json:"description,omitempty"` + + // The ID of the FlavorProfile which give the metadata for the creation of + // a LoadBalancer. + FlavorProfileId string `json:"flavor_profile_id" required:"true"` + + // If the resource is available for use. The default is True. + Enabled bool `json:"enabled,omitempty"` +} + +// ToFlavorCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToFlavorCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "flavor") +} + +// Create is and operation which add a new Flavor into the database. +// CreateResult will be returned. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFlavorCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular Flavor based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToFlavorUpdateMap() (map[string]any, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + + // Human-readable description for the Flavor. + Description string `json:"description,omitempty"` + + // If the resource is available for use. + Enabled bool `json:"enabled,omitempty"` +} + +// ToFlavorUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToFlavorUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "flavor") + if err != nil { + return nil, err + } + + return b, nil +} + +// Update is an operation which modifies the attributes of the specified +// Flavor. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFlavorUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular Flavor based on its +// unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/results.go new file mode 100644 index 000000000..105a6cfec --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/results.go @@ -0,0 +1,98 @@ +package flavors + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Flavor provide specs for the creation of a load balancer. +type Flavor struct { + // The unique ID for the Flavor + ID string `json:"id"` + + // Human-readable name for the Flavor. Does not have to be unique. + Name string `json:"name"` + + // Human-readable description for the Flavor. + Description string `json:"description"` + + // Status of the Flavor. + Enabled bool `json:"enabled"` + + // Flavor Profile apply to this Flavor. + FlavorProfileId string `json:"flavor_profile_id"` +} + +// FlavorPage is the page returned by a pager when traversing over a +// collection of flavors. +type FlavorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of flavors has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r FlavorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"flavors_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a FlavorPage struct is empty. +func (r FlavorPage) IsEmpty() (bool, error) { + is, err := ExtractFlavors(r) + return len(is) == 0, err +} + +// ExtractFlavors accepts a Page struct, specifically a FlavorPage +// struct, and extracts the elements into a slice of Flavor structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractFlavors(r pagination.Page) ([]Flavor, error) { + var s struct { + Flavors []Flavor `json:"flavors"` + } + err := (r.(FlavorPage)).ExtractInto(&s) + return s.Flavors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a flavor. +func (r commonResult) Extract() (*Flavor, error) { + var s struct { + Flavor *Flavor `json:"flavor"` + } + err := r.ExtractInto(&s) + return s.Flavor, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Flavor. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Flavor. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Flavor. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/urls.go new file mode 100644 index 000000000..1bfdfef87 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors/urls.go @@ -0,0 +1,16 @@ +package flavors + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "lbaas" + resourcePath = "flavors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/doc.go new file mode 100644 index 000000000..1b02b9230 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/doc.go @@ -0,0 +1,123 @@ +/* +Package l7policies provides information and interaction with L7Policies and +Rules of the LBaaS v2 extension for the OpenStack Networking service. + +Example to Create a L7Policy + + createOpts := l7policies.CreateOpts{ + Name: "redirect-example.com", + ListenerID: "023f2e34-7806-443b-bfae-16c324569a3d", + Action: l7policies.ActionRedirectToURL, + RedirectURL: "http://www.example.com", + } + l7policy, err := l7policies.Create(context.TODO(), lbClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List L7Policies + + listOpts := l7policies.ListOpts{ + ListenerID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + allPages, err := l7policies.List(lbClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + allL7Policies, err := l7policies.ExtractL7Policies(allPages) + if err != nil { + panic(err) + } + for _, l7policy := range allL7Policies { + fmt.Printf("%+v\n", l7policy) + } + +Example to Get a L7Policy + + l7policy, err := l7policies.Get(context.TODO(), lbClient, "023f2e34-7806-443b-bfae-16c324569a3d").Extract() + if err != nil { + panic(err) + } + +Example to Delete a L7Policy + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := l7policies.Delete(context.TODO(), lbClient, l7policyID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update a L7Policy + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + name := "new-name" + updateOpts := l7policies.UpdateOpts{ + Name: &name, + } + l7policy, err := l7policies.Update(context.TODO(), lbClient, l7policyID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + createOpts := l7policies.CreateRuleOpts{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images*", + } + rule, err := l7policies.CreateRule(context.TODO(), lbClient, l7policyID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to List L7 Rules + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + listOpts := l7policies.ListRulesOpts{ + RuleType: l7policies.TypePath, + } + allPages, err := l7policies.ListRules(lbClient, l7policyID, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + allRules, err := l7policies.ExtractRules(allPages) + if err != nil { + panic(err) + } + for _, rule := allRules { + fmt.Printf("%+v\n", rule) + } + +Example to Get a l7 rule + + l7rule, err := l7policies.GetRule(context.TODO(), lbClient, "023f2e34-7806-443b-bfae-16c324569a3d", "53ad8ab8-40fa-11e8-a508-00224d6b7bc1").Extract() + if err != nil { + panic(err) + } + +Example to Delete a l7 rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + ruleID := "64dba99f-8af8-4200-8882-e32a0660f23e" + err := l7policies.DeleteRule(context.TODO(), lbClient, l7policyID, ruleID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update a Rule + + l7policyID := "d67d56a6-4a86-4688-a282-f46444705c64" + ruleID := "64dba99f-8af8-4200-8882-e32a0660f23e" + updateOpts := l7policies.UpdateRuleOpts{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images/special*", + } + rule, err := l7policies.UpdateRule(context.TODO(), lbClient, l7policyID, ruleID, updateOpts).Extract() + if err != nil { + panic(err) + } +*/ +package l7policies diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/requests.go new file mode 100644 index 000000000..ab0b22c6b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/requests.go @@ -0,0 +1,441 @@ +package l7policies + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToL7PolicyCreateMap() (map[string]any, error) +} + +type Action string +type RuleType string +type CompareType string + +const ( + ActionRedirectPrefix Action = "REDIRECT_PREFIX" + ActionRedirectToPool Action = "REDIRECT_TO_POOL" + ActionRedirectToURL Action = "REDIRECT_TO_URL" + ActionReject Action = "REJECT" + + TypeCookie RuleType = "COOKIE" + TypeFileType RuleType = "FILE_TYPE" + TypeHeader RuleType = "HEADER" + TypeHostName RuleType = "HOST_NAME" + TypePath RuleType = "PATH" + TypeSSLConnHasCert RuleType = "SSL_CONN_HAS_CERT" + TypeSSLVerifyResult RuleType = "SSL_VERIFY_RESULT" + TypeSSLDNField RuleType = "SSL_DN_FIELD" + + CompareTypeContains CompareType = "CONTAINS" + CompareTypeEndWith CompareType = "ENDS_WITH" + CompareTypeEqual CompareType = "EQUAL_TO" + CompareTypeRegex CompareType = "REGEX" + CompareTypeStartWith CompareType = "STARTS_WITH" +) + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Name of the L7 policy. + Name string `json:"name,omitempty"` + + // The ID of the listener. + ListenerID string `json:"listener_id,omitempty"` + + // The L7 policy action. One of REDIRECT_PREFIX, REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action Action `json:"action" required:"true"` + + // The position of this policy on the listener. + Position int32 `json:"position,omitempty"` + + // A human-readable description for the resource. + Description string `json:"description,omitempty"` + + // ProjectID is the UUID of the project who owns the L7 policy in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // Requests matching this policy will be redirected to this Prefix URL. + // Only valid if action is REDIRECT_PREFIX. + RedirectPrefix string `json:"redirect_prefix,omitempty"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID string `json:"redirect_pool_id,omitempty"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL string `json:"redirect_url,omitempty"` + + // Requests matching this policy will be redirected to the specified URL or Prefix URL + // with the HTTP response code. Valid if action is REDIRECT_TO_URL or REDIRECT_PREFIX. + // Valid options are: 301, 302, 303, 307, or 308. Default is 302. Requires version 2.9 + RedirectHttpCode int32 `json:"redirect_http_code,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Rules is a slice of CreateRuleOpts which allows a set of rules + // to be created at the same time the policy is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + Rules []CreateRuleOpts `json:"rules,omitempty"` + + // Tags is a set of resource tags. Requires version 2.5. + Tags []string `json:"tags,omitempty"` +} + +// ToL7PolicyCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToL7PolicyCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "l7policy") +} + +// Create accepts a CreateOpts struct and uses the values to create a new l7policy. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToL7PolicyCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToL7PolicyListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. +type ListOpts struct { + Name string `q:"name"` + Description string `q:"description"` + ListenerID string `q:"listener_id"` + Action string `q:"action"` + ProjectID string `q:"project_id"` + RedirectPoolID string `q:"redirect_pool_id"` + RedirectURL string `q:"redirect_url"` + Position int32 `q:"position"` + AdminStateUp bool `q:"admin_state_up"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToL7PolicyListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToL7PolicyListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// l7policies. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those l7policies that are owned by the +// project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToL7PolicyListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return L7PolicyPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a particular l7policy based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular l7policy based on its unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToL7PolicyUpdateMap() (map[string]any, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Name of the L7 policy, empty string is allowed. + Name *string `json:"name,omitempty"` + + // The L7 policy action. One of REDIRECT_PREFIX, REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action Action `json:"action,omitempty"` + + // The position of this policy on the listener. + Position int32 `json:"position,omitempty"` + + // A human-readable description for the resource, empty string is allowed. + Description *string `json:"description,omitempty"` + + // Requests matching this policy will be redirected to this Prefix URL. + // Only valid if action is REDIRECT_PREFIX. + RedirectPrefix *string `json:"redirect_prefix,omitempty"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID *string `json:"redirect_pool_id,omitempty"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL *string `json:"redirect_url,omitempty"` + + // Requests matching this policy will be redirected to the specified URL or Prefix URL + // with the HTTP response code. Valid if action is REDIRECT_TO_URL or REDIRECT_PREFIX. + // Valid options are: 301, 302, 303, 307, or 308. Default is 302. Requires version 2.9 + RedirectHttpCode int32 `json:"redirect_http_code,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Tags is a set of resource tags. Requires version 2.5. + Tags *[]string `json:"tags,omitempty"` +} + +// ToL7PolicyUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToL7PolicyUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "l7policy") + if err != nil { + return nil, err + } + + m := b["l7policy"].(map[string]any) + + if m["redirect_pool_id"] == "" { + m["redirect_pool_id"] = nil + } + + if m["redirect_url"] == "" { + m["redirect_url"] = nil + } + + if m["redirect_prefix"] == "" { + m["redirect_prefix"] = nil + } + + if m["redirect_http_code"] == 0 { + m["redirect_http_code"] = nil + } + + return b, nil +} + +// Update allows l7policy to be updated. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToL7PolicyUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateRuleOptsBuilder allows extensions to add additional parameters to the +// CreateRule request. +type CreateRuleOptsBuilder interface { + ToRuleCreateMap() (map[string]any, error) +} + +// CreateRuleOpts is the common options struct used in this package's CreateRule +// operation. +type CreateRuleOpts struct { + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType RuleType `json:"type" required:"true"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType CompareType `json:"compare_type" required:"true"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value" required:"true"` + + // ProjectID is the UUID of the project who owns the rule in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key string `json:"key,omitempty"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert bool `json:"invert,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Tags is a set of resource tags. Requires version 2.5. + Tags []string `json:"tags,omitempty"` +} + +// ToRuleCreateMap builds a request body from CreateRuleOpts. +func (opts CreateRuleOpts) ToRuleCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "rule") +} + +// CreateRule will create and associate a Rule with a particular L7Policy. +func CreateRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, opts CreateRuleOptsBuilder) (r CreateRuleResult) { + b, err := opts.ToRuleCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, ruleRootURL(c, policyID), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListRulesOptsBuilder allows extensions to add additional parameters to the +// ListRules request. +type ListRulesOptsBuilder interface { + ToRulesListQuery() (string, error) +} + +// ListRulesOpts allows the filtering and sorting of paginated collections +// through the API. +type ListRulesOpts struct { + RuleType RuleType `q:"type"` + ProjectID string `q:"project_id"` + CompareType CompareType `q:"compare_type"` + Value string `q:"value"` + Key string `q:"key"` + Invert bool `q:"invert"` + AdminStateUp bool `q:"admin_state_up"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToRulesListQuery formats a ListOpts into a query string. +func (opts ListRulesOpts) ToRulesListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListRules returns a Pager which allows you to iterate over a collection of +// rules. It accepts a ListRulesOptsBuilder, which allows you to filter and +// sort the returned collection for greater efficiency. +// +// Default policy settings return only those rules that are owned by the +// project who submits the request, unless an admin user submits the request. +func ListRules(c *gophercloud.ServiceClient, policyID string, opts ListRulesOptsBuilder) pagination.Pager { + url := ruleRootURL(c, policyID) + if opts != nil { + query, err := opts.ToRulesListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return RulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// GetRule retrieves a particular L7Policy Rule based on its unique ID. +func GetRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, ruleID string) (r GetRuleResult) { + resp, err := c.Get(ctx, ruleResourceURL(c, policyID, ruleID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteRule will remove a Rule from a particular L7Policy. +func DeleteRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, ruleID string) (r DeleteRuleResult) { + resp, err := c.Delete(ctx, ruleResourceURL(c, policyID, ruleID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateRuleOptsBuilder allows to add additional parameters to the PUT request. +type UpdateRuleOptsBuilder interface { + ToRuleUpdateMap() (map[string]any, error) +} + +// UpdateRuleOpts is the common options struct used in this package's Update +// operation. +type UpdateRuleOpts struct { + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType RuleType `json:"type,omitempty"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType CompareType `json:"compare_type,omitempty"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value,omitempty"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key *string `json:"key,omitempty"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert *bool `json:"invert,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Tags is a set of resource tags. Requires version 2.5. + Tags *[]string `json:"tags,omitempty"` +} + +// ToRuleUpdateMap builds a request body from UpdateRuleOpts. +func (opts UpdateRuleOpts) ToRuleUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "rule") + if err != nil { + return nil, err + } + + if m := b["rule"].(map[string]any); m["key"] == "" { + m["key"] = nil + } + + return b, nil +} + +// UpdateRule allows Rule to be updated. +func UpdateRule(ctx context.Context, c *gophercloud.ServiceClient, policyID string, ruleID string, opts UpdateRuleOptsBuilder) (r UpdateRuleResult) { + b, err := opts.ToRuleUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, ruleResourceURL(c, policyID, ruleID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/results.go new file mode 100644 index 000000000..214d59d12 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/results.go @@ -0,0 +1,261 @@ +package l7policies + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// L7Policy is a collection of L7 rules associated with a Listener, and which +// may also have an association to a back-end pool. +type L7Policy struct { + // The unique ID for the L7 policy. + ID string `json:"id"` + + // Name of the L7 policy. + Name string `json:"name"` + + // The ID of the listener. + ListenerID string `json:"listener_id"` + + // The L7 policy action. One of REDIRECT_PREFIX, REDIRECT_TO_POOL, REDIRECT_TO_URL, or REJECT. + Action string `json:"action"` + + // The position of this policy on the listener. + Position int32 `json:"position"` + + // A human-readable description for the resource. + Description string `json:"description"` + + // ProjectID is the UUID of the project who owns the L7 policy in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id"` + + // Requests matching this policy will be redirected to the pool with this ID. + // Only valid if action is REDIRECT_TO_POOL. + RedirectPoolID string `json:"redirect_pool_id"` + + // Requests matching this policy will be redirected to this Prefix URL. + // Only valid if action is REDIRECT_PREFIX. + RedirectPrefix string `json:"redirect_prefix"` + + // Requests matching this policy will be redirected to this URL. + // Only valid if action is REDIRECT_TO_URL. + RedirectURL string `json:"redirect_url"` + + // Requests matching this policy will be redirected to the specified URL or Prefix URL + // with the HTTP response code. Valid if action is REDIRECT_TO_URL or REDIRECT_PREFIX. + RedirectHttpCode int32 `json:"redirect_http_code"` + + // The administrative state of the L7 policy, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The provisioning status of the L7 policy. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the L7 policy. + OperatingStatus string `json:"operating_status"` + + // Rules are List of associated L7 rule IDs. + Rules []Rule `json:"rules"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. + Tags []string `json:"tags"` +} + +// Rule represents layer 7 load balancing rule. +type Rule struct { + // The unique ID for the L7 rule. + ID string `json:"id"` + + // The L7 rule type. One of COOKIE, FILE_TYPE, HEADER, HOST_NAME, or PATH. + RuleType string `json:"type"` + + // The comparison type for the L7 rule. One of CONTAINS, ENDS_WITH, EQUAL_TO, REGEX, or STARTS_WITH. + CompareType string `json:"compare_type"` + + // The value to use for the comparison. For example, the file type to compare. + Value string `json:"value"` + + // ProjectID is the UUID of the project who owns the rule in octavia. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id"` + + // The key to use for the comparison. For example, the name of the cookie to evaluate. + Key string `json:"key"` + + // When true the logic of the rule is inverted. For example, with invert true, + // equal to would become not equal to. Default is false. + Invert bool `json:"invert"` + + // The administrative state of the L7 rule, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The provisioning status of the L7 rule. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the L7 policy. + OperatingStatus string `json:"operating_status"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. + Tags []string `json:"tags"` +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a l7policy. +func (r commonResult) Extract() (*L7Policy, error) { + var s struct { + L7Policy *L7Policy `json:"l7policy"` + } + err := r.ExtractInto(&s) + return s.L7Policy, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret the result as a L7Policy. +type CreateResult struct { + commonResult +} + +// L7PolicyPage is the page returned by a pager when traversing over a +// collection of l7policies. +type L7PolicyPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of l7policies has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r L7PolicyPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"l7policies_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a L7PolicyPage struct is empty. +func (r L7PolicyPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractL7Policies(r) + return len(is) == 0, err +} + +// ExtractL7Policies accepts a Page struct, specifically a L7PolicyPage struct, +// and extracts the elements into a slice of L7Policy structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractL7Policies(r pagination.Page) ([]L7Policy, error) { + var s struct { + L7Policies []L7Policy `json:"l7policies"` + } + err := (r.(L7PolicyPage)).ExtractInto(&s) + return s.L7Policies, err +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret the result as a L7Policy. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret the result as a L7Policy. +type UpdateResult struct { + commonResult +} + +type commonRuleResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a rule. +func (r commonRuleResult) Extract() (*Rule, error) { + var s struct { + Rule *Rule `json:"rule"` + } + err := r.ExtractInto(&s) + return s.Rule, err +} + +// CreateRuleResult represents the result of a CreateRule operation. +// Call its Extract method to interpret it as a Rule. +type CreateRuleResult struct { + commonRuleResult +} + +// RulePage is the page returned by a pager when traversing over a +// collection of Rules in a L7Policy. +type RulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of rules has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r RulePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RulePage struct is empty. +func (r RulePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractRules(r) + return len(is) == 0, err +} + +// ExtractRules accepts a Page struct, specifically a RulePage struct, +// and extracts the elements into a slice of Rules structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]Rule, error) { + var s struct { + Rules []Rule `json:"rules"` + } + err := (r.(RulePage)).ExtractInto(&s) + return s.Rules, err +} + +// GetRuleResult represents the result of a GetRule operation. +// Call its Extract method to interpret it as a Rule. +type GetRuleResult struct { + commonRuleResult +} + +// DeleteRuleResult represents the result of a DeleteRule operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteRuleResult struct { + gophercloud.ErrResult +} + +// UpdateRuleResult represents the result of an UpdateRule operation. +// Call its Extract method to interpret it as a Rule. +type UpdateRuleResult struct { + commonRuleResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/urls.go new file mode 100644 index 000000000..57126a881 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies/urls.go @@ -0,0 +1,25 @@ +package l7policies + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "lbaas" + resourcePath = "l7policies" + rulePath = "rules" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func ruleRootURL(c *gophercloud.ServiceClient, policyID string) string { + return c.ServiceURL(rootPath, resourcePath, policyID, rulePath) +} + +func ruleResourceURL(c *gophercloud.ServiceClient, policyID string, ruleID string) string { + return c.ServiceURL(rootPath, resourcePath, policyID, rulePath, ruleID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/doc.go new file mode 100644 index 000000000..2659715f8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/doc.go @@ -0,0 +1,77 @@ +/* +Package listeners provides information and interaction with Listeners of the +LBaaS v2 extension for the OpenStack Networking service. + +Example to List Listeners + + listOpts := listeners.ListOpts{ + LoadbalancerID : "ca430f80-1737-4712-8dc6-3f640d55594b", + } + + allPages, err := listeners.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allListeners, err := listeners.ExtractListeners(allPages) + if err != nil { + panic(err) + } + + for _, listener := range allListeners { + fmt.Printf("%+v\n", listener) + } + +Example to Create a Listener + + createOpts := listeners.CreateOpts{ + Protocol: "TCP", + Name: "db", + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + AdminStateUp: gophercloud.Enabled, + DefaultPoolID: "41efe233-7591-43c5-9cf7-923964759f9e", + ProtocolPort: 3306, + Tags: []string{"test", "stage"}, + } + + listener, err := listeners.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + + i1001 := 1001 + i181000 := 181000 + newTags := []string{"prod"} + updateOpts := listeners.UpdateOpts{ + ConnLimit: &i1001, + TimeoutClientData: &i181000, + TimeoutMemberData: &i181000, + Tags: &newTags, + } + + listener, err := listeners.Update(context.TODO(), networkClient, listenerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := listeners.Delete(context.TODO(), networkClient, listenerID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Get the Statistics of a Listener + + listenerID := "d67d56a6-4a86-4688-a282-f46444705c64" + stats, err := listeners.GetStats(context.TODO(), networkClient, listenerID).Extract() + if err != nil { + panic(err) + } +*/ +package listeners diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/requests.go new file mode 100644 index 000000000..abd5d0897 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/requests.go @@ -0,0 +1,408 @@ +package listeners + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Type Protocol represents a listener protocol. +type Protocol string + +// Supported attributes for create/update operations. +const ( + ProtocolTCP Protocol = "TCP" + ProtocolUDP Protocol = "UDP" + ProtocolPROXY Protocol = "PROXY" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" + // Protocol SCTP requires octavia microversion 2.23 + ProtocolSCTP Protocol = "SCTP" + // Protocol Prometheus requires octavia microversion 2.25 + ProtocolPrometheus Protocol = "PROMETHEUS" + ProtocolTerminatedHTTPS Protocol = "TERMINATED_HTTPS" +) + +// Type TLSVersion represents a tls version +type TLSVersion string + +const ( + TLSVersionSSLv3 TLSVersion = "SSLv3" + TLSVersionTLSv1 TLSVersion = "TLSv1" + TLSVersionTLSv1_1 TLSVersion = "TLSv1.1" + TLSVersionTLSv1_2 TLSVersion = "TLSv1.2" + TLSVersionTLSv1_3 TLSVersion = "TLSv1.3" +) + +// ClientAuthentication represents the TLS client authentication mode. +type ClientAuthentication string + +const ( + ClientAuthenticationNone ClientAuthentication = "NONE" + ClientAuthenticationOptional ClientAuthentication = "OPTIONAL" + ClientAuthenticationMandatory ClientAuthentication = "MANDATORY" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToListenerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular listener attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + AdminStateUp *bool `q:"admin_state_up"` + ProjectID string `q:"project_id"` + LoadbalancerID string `q:"loadbalancer_id"` + DefaultPoolID string `q:"default_pool_id"` + Protocol string `q:"protocol"` + ProtocolPort int `q:"protocol_port"` + ConnectionLimit int `q:"connection_limit"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + TimeoutClientData *int `q:"timeout_client_data"` + TimeoutMemberData *int `q:"timeout_member_data"` + TimeoutMemberConnect *int `q:"timeout_member_connect"` + TimeoutTCPInspect *int `q:"timeout_tcp_inspect"` + Tags []string `q:"tags"` +} + +// ToListenerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToListenerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// listeners. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those listeners that are owned by the +// project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToListenerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ListenerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToListenerCreateMap() (map[string]any, error) +} + +// CreateOpts represents options for creating a listener. +type CreateOpts struct { + // The load balancer on which to provision this listener. + LoadbalancerID string `json:"loadbalancer_id,omitempty"` + + // The protocol - can either be TCP, SCTP, HTTP, HTTPS or TERMINATED_HTTPS. + Protocol Protocol `json:"protocol" required:"true"` + + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + + // ProjectID is only required if the caller has an admin role and wants + // to create a pool for another project. + ProjectID string `json:"project_id,omitempty"` + + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name,omitempty"` + + // The ID of the default pool with which the Listener is associated. + DefaultPoolID string `json:"default_pool_id,omitempty"` + + // DefaultPool an instance of pools.CreateOpts which allows a + // (default) pool to be created at the same time the listener is created. + // + // This is only possible to use when creating a fully populated + // load balancer. + DefaultPool *pools.CreateOpts `json:"default_pool,omitempty"` + + // Human-readable description for the Listener. + Description string `json:"description,omitempty"` + + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + + // A reference to a Barbican container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref,omitempty"` + + // A list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs,omitempty"` + + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // L7Policies is a slice of l7policies.CreateOpts which allows a set + // of policies to be created at the same time the listener is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + L7Policies []l7policies.CreateOpts `json:"l7policies,omitempty"` + + // Frontend client inactivity timeout in milliseconds + TimeoutClientData *int `json:"timeout_client_data,omitempty"` + + // Backend member inactivity timeout in milliseconds + TimeoutMemberData *int `json:"timeout_member_data,omitempty"` + + // Backend member connection timeout in milliseconds + TimeoutMemberConnect *int `json:"timeout_member_connect,omitempty"` + + // Time, in milliseconds, to wait for additional TCP packets for content inspection + TimeoutTCPInspect *int `json:"timeout_tcp_inspect,omitempty"` + + // A dictionary of optional headers to insert into the request before it is sent to the backend member. + InsertHeaders map[string]string `json:"insert_headers,omitempty"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs []string `json:"allowed_cidrs,omitempty"` + + // A list of ALPN protocols. Available protocols: http/1.0, http/1.1, + // h2. Available from microversion 2.20. + ALPNProtocols []string `json:"alpn_protocols,omitempty"` + + // The TLS client authentication mode. One of the options NONE, + // OPTIONAL or MANDATORY. Available from microversion 2.8. + ClientAuthentication ClientAuthentication `json:"client_authentication,omitempty"` + + // The ref of the key manager service secret containing a PEM format + // client CA certificate bundle for TERMINATED_HTTPS listeners. + // Available from microversion 2.8. + ClientCATLSContainerRef string `json:"client_ca_tls_container_ref,omitempty"` + + // The URI of the key manager service secret containing a PEM format CA + // revocation list file for TERMINATED_HTTPS listeners. Available from + // microversion 2.8. + ClientCRLContainerRef string `json:"client_crl_container_ref,omitempty"` + + // Defines whether the includeSubDomains directive should be added to + // the Strict-Transport-Security HTTP response header. This requires + // setting the hsts_max_age option as well in order to become + // effective. Available from microversion 2.27. + HSTSIncludeSubdomains bool `json:"hsts_include_subdomains,omitempty"` + + // The value of the max_age directive for the Strict-Transport-Security + // HTTP response header. Setting this enables HTTP Strict Transport + // Security (HSTS) for the TLS-terminated listener. Available from + // microversion 2.27. + HSTSMaxAge int `json:"hsts_max_age,omitempty"` + + // Defines whether the preload directive should be added to the + // Strict-Transport-Security HTTP response header. This requires + // setting the hsts_max_age option as well in order to become + // effective. Available from microversion 2.27. + HSTSPreload bool `json:"hsts_preload,omitempty"` + + // List of ciphers in OpenSSL format (colon-separated). Available from + // microversion 2.15. + TLSCiphers string `json:"tls_ciphers,omitempty"` + + // A list of TLS protocol versions. Available from microversion 2.17 + TLSVersions []TLSVersion `json:"tls_versions,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags []string `json:"tags,omitempty"` +} + +// ToListenerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToListenerCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "listener") +} + +// Create is an operation which provisions a new Listeners based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +// +// Users with an admin role can create Listeners on behalf of other projects by +// specifying a ProjectID attribute different than their own. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToListenerCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular Listeners based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToListenerUpdateMap() (map[string]any, error) +} + +// UpdateOpts represents options for updating a Listener. +type UpdateOpts struct { + // Human-readable name for the Listener. Does not have to be unique. + Name *string `json:"name,omitempty"` + + // The ID of the default pool with which the Listener is associated. + DefaultPoolID *string `json:"default_pool_id,omitempty"` + + // Human-readable description for the Listener. + Description *string `json:"description,omitempty"` + + // The maximum number of connections allowed for the Listener. + ConnLimit *int `json:"connection_limit,omitempty"` + + // A reference to a Barbican container of TLS secrets. + DefaultTlsContainerRef *string `json:"default_tls_container_ref,omitempty"` + + // A list of references to TLS secrets. + SniContainerRefs *[]string `json:"sni_container_refs,omitempty"` + + // The administrative state of the Listener. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Frontend client inactivity timeout in milliseconds + TimeoutClientData *int `json:"timeout_client_data,omitempty"` + + // Backend member inactivity timeout in milliseconds + TimeoutMemberData *int `json:"timeout_member_data,omitempty"` + + // Backend member connection timeout in milliseconds + TimeoutMemberConnect *int `json:"timeout_member_connect,omitempty"` + + // Time, in milliseconds, to wait for additional TCP packets for content inspection + TimeoutTCPInspect *int `json:"timeout_tcp_inspect,omitempty"` + + // A dictionary of optional headers to insert into the request before it is sent to the backend member. + InsertHeaders *map[string]string `json:"insert_headers,omitempty"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs *[]string `json:"allowed_cidrs,omitempty"` + + // A list of ALPN protocols. Available protocols: http/1.0, http/1.1, + // h2. Available from microversion 2.20. + ALPNProtocols *[]string `json:"alpn_protocols,omitempty"` + + // The TLS client authentication mode. One of the options NONE, + // OPTIONAL or MANDATORY. Available from microversion 2.8. + ClientAuthentication *ClientAuthentication `json:"client_authentication,omitempty"` + + // The ref of the key manager service secret containing a PEM format + // client CA certificate bundle for TERMINATED_HTTPS listeners. + // Available from microversion 2.8. + ClientCATLSContainerRef *string `json:"client_ca_tls_container_ref,omitempty"` + + // The URI of the key manager service secret containing a PEM format CA + // revocation list file for TERMINATED_HTTPS listeners. Available from + // microversion 2.8. + ClientCRLContainerRef *string `json:"client_crl_container_ref,omitempty"` + + // Defines whether the includeSubDomains directive should be added to + // the Strict-Transport-Security HTTP response header. This requires + // setting the hsts_max_age option as well in order to become + // effective. Available from microversion 2.27. + HSTSIncludeSubdomains *bool `json:"hsts_include_subdomains,omitempty"` + + // The value of the max_age directive for the Strict-Transport-Security + // HTTP response header. Setting this enables HTTP Strict Transport + // Security (HSTS) for the TLS-terminated listener. Available from + // microversion 2.27. + HSTSMaxAge *int `json:"hsts_max_age,omitempty"` + + // Defines whether the preload directive should be added to the + // Strict-Transport-Security HTTP response header. This requires + // setting the hsts_max_age option as well in order to become + // effective. Available from microversion 2.27. + HSTSPreload *bool `json:"hsts_preload,omitempty"` + + // List of ciphers in OpenSSL format (colon-separated). Available from + // microversion 2.15. + TLSCiphers *string `json:"tls_ciphers,omitempty"` + + // A list of TLS protocol versions. Available from microversion 2.17 + TLSVersions *[]TLSVersion `json:"tls_versions,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags *[]string `json:"tags,omitempty"` +} + +// ToListenerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToListenerUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "listener") + if err != nil { + return nil, err + } + + m := b["listener"].(map[string]any) + + // allow to unset default_pool_id on empty string + if m["default_pool_id"] == "" { + m["default_pool_id"] = nil + } + + // allow to unset alpn_protocols on empty slice + if opts.ALPNProtocols != nil && len(*opts.ALPNProtocols) == 0 { + m["alpn_protocols"] = nil + } + + // allow to unset tls_versions on empty slice + if opts.TLSVersions != nil && len(*opts.TLSVersions) == 0 { + m["tls_versions"] = nil + } + + return b, nil +} + +// Update is an operation which modifies the attributes of the specified +// Listener. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToListenerUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular Listeners based on its unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetStats will return the shows the current statistics of a particular Listeners. +func GetStats(ctx context.Context, c *gophercloud.ServiceClient, id string) (r StatsResult) { + resp, err := c.Get(ctx, statisticsRootURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/results.go new file mode 100644 index 000000000..0bd08f783 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/results.go @@ -0,0 +1,248 @@ +package listeners + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type LoadBalancerID struct { + ID string `json:"id"` +} + +// Listener is the primary load balancing configuration object that specifies +// the loadbalancer and port on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type Listener struct { + // The unique ID for the Listener. + ID string `json:"id"` + + // Owner of the Listener. + ProjectID string `json:"project_id"` + + // Human-readable name for the Listener. Does not have to be unique. + Name string `json:"name"` + + // Human-readable description for the Listener. + Description string `json:"description"` + + // The protocol to loadbalance. A valid value is TCP, SCTP, HTTP, HTTPS or TERMINATED_HTTPS. + Protocol string `json:"protocol"` + + // The port on which to listen to client traffic that is associated with the + // Loadbalancer. A valid value is from 0 to 65535. + ProtocolPort int `json:"protocol_port"` + + // The UUID of default pool. Must have compatible protocol with listener. + DefaultPoolID string `json:"default_pool_id"` + + // The default pool with which the Listener is associated. + DefaultPool *pools.Pool `json:"default_pool"` + + // A list of load balancer IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + + // The maximum number of connections allowed for the Loadbalancer. + // Default is -1, meaning no limit. + ConnLimit int `json:"connection_limit"` + + // The list of references to TLS secrets. + SniContainerRefs []string `json:"sni_container_refs"` + + // A reference to a Barbican container of TLS secrets. + DefaultTlsContainerRef string `json:"default_tls_container_ref"` + + // The administrative state of the Listener. A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + + // Pools are the pools which are part of this listener. + Pools []pools.Pool `json:"pools"` + + // L7policies are the L7 policies which are part of this listener. + L7Policies []l7policies.L7Policy `json:"l7policies"` + + // The provisioning status of the Listener. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // Frontend client inactivity timeout in milliseconds + TimeoutClientData int `json:"timeout_client_data"` + + // Backend member inactivity timeout in milliseconds + TimeoutMemberData int `json:"timeout_member_data"` + + // Backend member connection timeout in milliseconds + TimeoutMemberConnect int `json:"timeout_member_connect"` + + // Time, in milliseconds, to wait for additional TCP packets for content inspection + TimeoutTCPInspect int `json:"timeout_tcp_inspect"` + + // A dictionary of optional headers to insert into the request before it is sent to the backend member. + InsertHeaders map[string]string `json:"insert_headers"` + + // A list of IPv4, IPv6 or mix of both CIDRs + AllowedCIDRs []string `json:"allowed_cidrs"` + + // List of ciphers in OpenSSL format (colon-separated). See + // https://www.openssl.org/docs/man1.1.1/man1/ciphers.html + // New in version 2.15 + TLSCiphers string `json:"tls_ciphers"` + + // A list of TLS protocol versions. Available from microversion 2.17 + TLSVersions []string `json:"tls_versions"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. New in version 2.5 + Tags []string `json:"tags"` + + // A list of ALPN protocols. Available protocols: http/1.0, http/1.1, h2 + // New in version 2.20 + ALPNProtocols []string `json:"alpn_protocols"` + + // The TLS client authentication mode. One of the options NONE, OPTIONAL or MANDATORY. + // New in version 2.8 + ClientAuthentication string `json:"client_authentication"` + + // The ref of the key manager service secret containing a PEM format + // client CA certificate bundle for TERMINATED_HTTPS listeners. + // New in version 2.8 + ClientCATLSContainerRef string `json:"client_ca_tls_container_ref"` + + // The URI of the key manager service secret containing a PEM format CA + // revocation list file for TERMINATED_HTTPS listeners. + // New in version 2.8 + ClientCRLContainerRef string `json:"client_crl_container_ref"` + + // Defines whether the includeSubDomains directive should be added to + // the Strict-Transport-Security HTTP response header. This requires + // setting the hsts_max_age option as well in order to become + // effective. Available from microversion 2.27. + HSTSIncludeSubdomains bool `json:"hsts_include_subdomains"` + + // The value of the max_age directive for the Strict-Transport-Security + // HTTP response header. Setting this enables HTTP Strict Transport + // Security (HSTS) for the TLS-terminated listener. Available from + // microversion 2.27. + HSTSMaxAge int `json:"hsts_max_age"` + + // Defines whether the preload directive should be added to the + // Strict-Transport-Security HTTP response header. This requires + // setting the hsts_max_age option as well in order to become + // effective. Available from microversion 2.27. + HSTSPreload bool `json:"hsts_preload"` + + // The operating status of the resource + OperatingStatus string `json:"operating_status"` +} + +type Stats struct { + // The currently active connections. + ActiveConnections int `json:"active_connections"` + + // The total bytes received. + BytesIn int `json:"bytes_in"` + + // The total bytes sent. + BytesOut int `json:"bytes_out"` + + // The total requests that were unable to be fulfilled. + RequestErrors int `json:"request_errors"` + + // The total connections handled. + TotalConnections int `json:"total_connections"` +} + +// ListenerPage is the page returned by a pager when traversing over a +// collection of listeners. +type ListenerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of listeners has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r ListenerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"listeners_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a ListenerPage struct is empty. +func (r ListenerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractListeners(r) + return len(is) == 0, err +} + +// ExtractListeners accepts a Page struct, specifically a ListenerPage struct, +// and extracts the elements into a slice of Listener structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractListeners(r pagination.Page) ([]Listener, error) { + var s struct { + Listeners []Listener `json:"listeners"` + } + err := (r.(ListenerPage)).ExtractInto(&s) + return s.Listeners, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a listener. +func (r commonResult) Extract() (*Listener, error) { + var s struct { + Listener *Listener `json:"listener"` + } + err := r.ExtractInto(&s) + return s.Listener, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Listener. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Listener. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Listener. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// StatsResult represents the result of a GetStats operation. +// Call its Extract method to interpret it as a Stats. +type StatsResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Listener. +func (r StatsResult) Extract() (*Stats, error) { + var s struct { + Stats *Stats `json:"stats"` + } + err := r.ExtractInto(&s) + return s.Stats, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/urls.go new file mode 100644 index 000000000..77157c726 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners/urls.go @@ -0,0 +1,21 @@ +package listeners + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "lbaas" + resourcePath = "listeners" + statisticsPath = "stats" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func statisticsRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statisticsPath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/doc.go new file mode 100644 index 000000000..0e318558c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/doc.go @@ -0,0 +1,140 @@ +/* +Package loadbalancers provides information and interaction with Load Balancers +of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Load Balancers + + listOpts := loadbalancers.ListOpts{ + Provider: "haproxy", + } + + allPages, err := loadbalancers.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allLoadbalancers, err := loadbalancers.ExtractLoadBalancers(allPages) + if err != nil { + panic(err) + } + + for _, lb := range allLoadbalancers { + fmt.Printf("%+v\n", lb) + } + +Example to Create a Load Balancer + + createOpts := loadbalancers.CreateOpts{ + Name: "db_lb", + AdminStateUp: gophercloud.Enabled, + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + FlavorID: "60df399a-ee85-11e9-81b4-2a2ae2dbcce4", + Provider: "haproxy", + Tags: []string{"test", "stage"}, + } + + lb, err := loadbalancers.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a fully populated Load Balancer + + createOpts := loadbalancers.CreateOpts{ + Name: "db_lb", + AdminStateUp: gophercloud.Enabled, + VipSubnetID: "9cedb85d-0759-4898-8a4b-fa5a5ea10086", + VipAddress: "10.30.176.48", + FlavorID: "60df399a-ee85-11e9-81b4-2a2ae2dbcce4", + Provider: "haproxy", + Tags: []string{"test", "stage"}, + Listeners: []listeners.CreateOpts{{ + Protocol: "HTTP", + ProtocolPort: 8080, + Name: "redirect_listener", + L7Policies: []l7policies.CreateOpts{{ + Name: "redirect-example.com", + Action: l7policies.ActionRedirectToURL, + RedirectURL: "http://www.example.com", + Rules: []l7policies.CreateRuleOpts{{ + RuleType: l7policies.TypePath, + CompareType: l7policies.CompareTypeRegex, + Value: "/images*", + }}, + }}, + DefaultPool: &pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "example pool", + Members: []pools.BatchUpdateMemberOpts{{ + Address: "192.0.2.51", + ProtocolPort: 80, + },}, + Monitor: &monitors.CreateOpts{ + Name: "db", + Type: "HTTP", + Delay: 3, + MaxRetries: 2, + Timeout: 1, + }, + }, + }}, + } + + lb, err := loadbalancers.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + name := "new-name" + updateOpts := loadbalancers.UpdateOpts{ + Name: &name, + } + lb, err := loadbalancers.Update(context.TODO(), networkClient, lbID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Load Balancers + + deleteOpts := loadbalancers.DeleteOpts{ + Cascade: true, + } + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + + err := loadbalancers.Delete(context.TODO(), networkClient, lbID, deleteOpts).ExtractErr() + if err != nil { + panic(err) + } + +Example to Get the Status of a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + status, err := loadbalancers.GetStatuses(context.TODO(), networkClient, LBID).Extract() + if err != nil { + panic(err) + } + +Example to Get the Statistics of a Load Balancer + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + stats, err := loadbalancers.GetStats(context.TODO(), networkClient, LBID).Extract() + if err != nil { + panic(err) + } + +Example to Failover a Load Balancers + + lbID := "d67d56a6-4a86-4688-a282-f46444705c64" + + err := loadbalancers.Failover(context.TODO(), networkClient, lbID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package loadbalancers diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/requests.go new file mode 100644 index 000000000..095170edd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/requests.go @@ -0,0 +1,281 @@ +package loadbalancers + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToLoadBalancerListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Loadbalancer attributes you want to see returned. SortKey allows you to +// sort by a particular attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + ProjectID string `q:"project_id"` + ProvisioningStatus string `q:"provisioning_status"` + VipAddress string `q:"vip_address"` + VipPortID string `q:"vip_port_id"` + VipSubnetID string `q:"vip_subnet_id"` + VipNetworkID string `q:"vip_network_id"` + ID string `q:"id"` + OperatingStatus string `q:"operating_status"` + Name string `q:"name"` + FlavorID string `q:"flavor_id"` + AvailabilityZone string `q:"availability_zone"` + Provider string `q:"provider"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags []string `q:"tags"` + TagsAny []string `q:"tags-any"` + TagsNot []string `q:"not-tags"` + TagsNotAny []string `q:"not-tags-any"` +} + +// ToLoadBalancerListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToLoadBalancerListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// load balancers. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +// +// Default policy settings return only those load balancers that are owned by +// the project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToLoadBalancerListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return LoadBalancerPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToLoadBalancerCreateMap() (map[string]any, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // Human-readable name for the Loadbalancer. Does not have to be unique. + Name string `json:"name,omitempty"` + + // Human-readable description for the Loadbalancer. + Description string `json:"description,omitempty"` + + // Providing a neutron port ID for the vip_port_id tells Octavia to use this + // port for the VIP. If the port has more than one subnet you must specify + // either the vip_subnet_id or vip_address to clarify which address should + // be used for the VIP. + VipPortID string `json:"vip_port_id,omitempty"` + + // The subnet on which to allocate the Loadbalancer's address. A project can + // only create Loadbalancers on networks authorized by policy (e.g. networks + // that belong to them or networks that are shared). + VipSubnetID string `json:"vip_subnet_id,omitempty"` + + // The network on which to allocate the Loadbalancer's address. A tenant can + // only create Loadbalancers on networks authorized by policy (e.g. networks + // that belong to them or networks that are shared). + VipNetworkID string `json:"vip_network_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Loadbalancer. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The IP address of the Loadbalancer. + VipAddress string `json:"vip_address,omitempty"` + + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID string `json:"vip_qos_policy_id,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // The UUID of a flavor. + FlavorID string `json:"flavor_id,omitempty"` + + // The name of an Octavia availability zone. + // Requires Octavia API version 2.14 or later. + AvailabilityZone string `json:"availability_zone,omitempty"` + + // The name of the provider. + Provider string `json:"provider,omitempty"` + + // Listeners is a slice of listeners.CreateOpts which allows a set + // of listeners to be created at the same time the Loadbalancer is created. + // + // This is only possible to use when creating a fully populated + // load balancer. + Listeners []listeners.CreateOpts `json:"listeners,omitempty"` + + // Pools is a slice of pools.CreateOpts which allows a set of pools + // to be created at the same time the Loadbalancer is created. + // + // This is only possible to use when creating a fully populated + // load balancer. + Pools []pools.CreateOpts `json:"pools,omitempty"` + + // Tags is a set of resource tags. + Tags []string `json:"tags,omitempty"` + + // The additional ips of the loadbalancer. Subnets must all belong to the same network as the primary VIP. + // New in version 2.26 + AdditionalVips []AdditionalVip `json:"additional_vips,omitempty"` +} + +// ToLoadBalancerCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToLoadBalancerCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Create is an operation which provisions a new loadbalancer based on the +// configuration defined in the CreateOpts struct. Once the request is +// validated and progress has started on the provisioning process, a +// CreateResult will be returned. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToLoadBalancerCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular Loadbalancer based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToLoadBalancerUpdateMap() (map[string]any, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Human-readable name for the Loadbalancer. Does not have to be unique. + Name *string `json:"name,omitempty"` + + // Human-readable description for the Loadbalancer. + Description *string `json:"description,omitempty"` + + // The administrative state of the Loadbalancer. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID *string `json:"vip_qos_policy_id,omitempty"` + + // Tags is a set of resource tags. + Tags *[]string `json:"tags,omitempty"` +} + +// ToLoadBalancerUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToLoadBalancerUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "loadbalancer") +} + +// Update is an operation which modifies the attributes of the specified +// LoadBalancer. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToLoadBalancerUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteOptsBuilder allows extensions to add additional parameters to the +// Delete request. +type DeleteOptsBuilder interface { + ToLoadBalancerDeleteQuery() (string, error) +} + +// DeleteOpts is the common options struct used in this package's Delete +// operation. +type DeleteOpts struct { + // Cascade will delete all children of the load balancer (listners, monitors, etc). + Cascade bool `q:"cascade"` +} + +// ToLoadBalancerDeleteQuery formats a DeleteOpts into a query string. +func (opts DeleteOpts) ToLoadBalancerDeleteQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// Delete will permanently delete a particular LoadBalancer based on its +// unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string, opts DeleteOptsBuilder) (r DeleteResult) { + url := resourceURL(c, id) + if opts != nil { + query, err := opts.ToLoadBalancerDeleteQuery() + if err != nil { + r.Err = err + return + } + url += query + } + resp, err := c.Delete(ctx, url, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetStatuses will return the status of a particular LoadBalancer. +func GetStatuses(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetStatusesResult) { + resp, err := c.Get(ctx, statusRootURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetStats will return the shows the current statistics of a particular LoadBalancer. +func GetStats(ctx context.Context, c *gophercloud.ServiceClient, id string) (r StatsResult) { + resp, err := c.Get(ctx, statisticsRootURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Failover performs a failover of a load balancer. +func Failover(ctx context.Context, c *gophercloud.ServiceClient, id string) (r FailoverResult) { + resp, err := c.Put(ctx, failoverRootURL(c, id), nil, nil, &gophercloud.RequestOpts{ + OkCodes: []int{202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/results.go new file mode 100644 index 000000000..67e5c749d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/results.go @@ -0,0 +1,266 @@ +package loadbalancers + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// LoadBalancer is the primary load balancing configuration object that +// specifies the virtual IP address on which client traffic is received, as well +// as other details such as the load balancing method to be use, protocol, etc. +type LoadBalancer struct { + // Human-readable description for the Loadbalancer. + Description string `json:"description"` + + // The administrative state of the Loadbalancer. + // A valid value is true (UP) or false (DOWN). + AdminStateUp bool `json:"admin_state_up"` + + // Owner of the LoadBalancer. + ProjectID string `json:"project_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // loadbalancer last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // The provisioning status of the LoadBalancer. + // This value is ACTIVE, PENDING_CREATE or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The IP address of the Loadbalancer. + VipAddress string `json:"vip_address"` + + // The UUID of the port associated with the IP address. + VipPortID string `json:"vip_port_id"` + + // The UUID of the subnet on which to allocate the virtual IP for the + // Loadbalancer address. + VipSubnetID string `json:"vip_subnet_id"` + + // The UUID of the network on which to allocate the virtual IP for the + // Loadbalancer address. + VipNetworkID string `json:"vip_network_id"` + + // The ID of the QoS Policy which will apply to the Virtual IP + VipQosPolicyID string `json:"vip_qos_policy_id"` + + // The unique ID for the LoadBalancer. + ID string `json:"id"` + + // The operating status of the LoadBalancer. This value is ONLINE or OFFLINE. + OperatingStatus string `json:"operating_status"` + + // Human-readable name for the LoadBalancer. Does not have to be unique. + Name string `json:"name"` + + // The UUID of a flavor if set. + FlavorID string `json:"flavor_id"` + + // The name of an Octavia availability zone if set. + AvailabilityZone string `json:"availability_zone"` + + // The name of the provider. + Provider string `json:"provider"` + + // Listeners are the listeners related to this Loadbalancer. + Listeners []listeners.Listener `json:"listeners"` + + // Pools are the pools related to this Loadbalancer. + Pools []pools.Pool `json:"pools"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. + Tags []string `json:"tags"` + + // The additional ips of the loadbalancer. Subnets must all belong to the same network as the primary VIP. + // New in version 2.26 + AdditionalVips []AdditionalVip `json:"additional_vips"` +} + +// AdditionalVip represent additional ip of a loadbalancer. IpAddress field is optional. +type AdditionalVip struct { + SubnetID string `json:"subnet_id"` + IPAddress string `json:"ip_address,omitempty"` +} + +func (r *LoadBalancer) UnmarshalJSON(b []byte) error { + type tmp LoadBalancer + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = LoadBalancer(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = LoadBalancer(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// StatusTree represents the status of a loadbalancer. +type StatusTree struct { + Loadbalancer *LoadBalancer `json:"loadbalancer"` +} + +type Stats struct { + // The currently active connections. + ActiveConnections int `json:"active_connections"` + + // The total bytes received. + BytesIn int `json:"bytes_in"` + + // The total bytes sent. + BytesOut int `json:"bytes_out"` + + // The total requests that were unable to be fulfilled. + RequestErrors int `json:"request_errors"` + + // The total connections handled. + TotalConnections int `json:"total_connections"` +} + +// LoadBalancerPage is the page returned by a pager when traversing over a +// collection of load balancers. +type LoadBalancerPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of load balancers has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r LoadBalancerPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"loadbalancers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a LoadBalancerPage struct is empty. +func (r LoadBalancerPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractLoadBalancers(r) + return len(is) == 0, err +} + +// ExtractLoadBalancers accepts a Page struct, specifically a LoadbalancerPage +// struct, and extracts the elements into a slice of LoadBalancer structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractLoadBalancers(r pagination.Page) ([]LoadBalancer, error) { + var s struct { + LoadBalancers []LoadBalancer `json:"loadbalancers"` + } + err := (r.(LoadBalancerPage)).ExtractInto(&s) + return s.LoadBalancers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a loadbalancer. +func (r commonResult) Extract() (*LoadBalancer, error) { + var s struct { + LoadBalancer *LoadBalancer `json:"loadbalancer"` + } + err := r.ExtractInto(&s) + return s.LoadBalancer, err +} + +// GetStatusesResult represents the result of a GetStatuses operation. +// Call its Extract method to interpret it as a StatusTree. +type GetStatusesResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Loadbalancer. +func (r GetStatusesResult) Extract() (*StatusTree, error) { + var s struct { + Statuses *StatusTree `json:"statuses"` + } + err := r.ExtractInto(&s) + return s.Statuses, err +} + +// StatsResult represents the result of a GetStats operation. +// Call its Extract method to interpret it as a Stats. +type StatsResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts the status of +// a Loadbalancer. +func (r StatsResult) Extract() (*Stats, error) { + var s struct { + Stats *Stats `json:"stats"` + } + err := r.ExtractInto(&s) + return s.Stats, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a LoadBalancer. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a LoadBalancer. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a LoadBalancer. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FailoverResult represents the result of a failover operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type FailoverResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/urls.go new file mode 100644 index 000000000..221bc84e3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers/urls.go @@ -0,0 +1,31 @@ +package loadbalancers + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "lbaas" + resourcePath = "loadbalancers" + statusPath = "status" + statisticsPath = "stats" + failoverPath = "failover" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func statusRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statusPath) +} + +func statisticsRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, statisticsPath) +} + +func failoverRootURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id, failoverPath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/doc.go new file mode 100644 index 000000000..f471084cf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/doc.go @@ -0,0 +1,71 @@ +/* +Package monitors provides information and interaction with Monitors +of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Monitors + + listOpts := monitors.ListOpts{ + PoolID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + + allPages, err := monitors.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allMonitors, err := monitors.ExtractMonitors(allPages) + if err != nil { + panic(err) + } + + for _, monitor := range allMonitors { + fmt.Printf("%+v\n", monitor) + } + +Example to Create a Monitor + + createOpts := monitors.CreateOpts{ + Type: "HTTP", + Name: "db", + PoolID: "84f1b61f-58c4-45bf-a8a9-2dafb9e5214d", + Delay: 20, + Timeout: 10, + MaxRetries: 5, + MaxRetriesDown: 4, + URLPath: "/check", + ExpectedCodes: "200-299", + } + + monitor, err := monitors.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Monitor + + monitorID := "d67d56a6-4a86-4688-a282-f46444705c64" + + updateOpts := monitors.UpdateOpts{ + Name: "NewHealthmonitorName", + Delay: 3, + Timeout: 20, + MaxRetries: 10, + MaxRetriesDown: 8, + URLPath: "/another_check", + ExpectedCodes: "301", + } + + monitor, err := monitors.Update(context.TODO(), networkClient, monitorID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Monitor + + monitorID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := monitors.Delete(context.TODO(), networkClient, monitorID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package monitors diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/requests.go new file mode 100644 index 000000000..15a503bad --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/requests.go @@ -0,0 +1,311 @@ +package monitors + +import ( + "context" + "strconv" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToMonitorListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Monitor attributes you want to see returned. SortKey allows you to +// sort by a particular Monitor attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + PoolID string `q:"pool_id"` + Type string `q:"type"` + Delay int `q:"delay"` + Timeout int `q:"timeout"` + MaxRetries int `q:"max_retries"` + MaxRetriesDown int `q:"max_retries_down"` + HTTPMethod string `q:"http_method"` + URLPath string `q:"url_path"` + ExpectedCodes string `q:"expected_codes"` + AdminStateUp *bool `q:"admin_state_up"` + Status string `q:"status"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags []string `q:"tags"` +} + +// ToMonitorListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToMonitorListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// health monitors. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those health monitors that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToMonitorListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MonitorPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Constants that represent approved monitoring types. +const ( + TypePING = "PING" + TypeTCP = "TCP" + TypeHTTP = "HTTP" + TypeHTTPS = "HTTPS" + TypeTLSHELLO = "TLS-HELLO" + TypeUDPConnect = "UDP-CONNECT" + TypeSCTP = "SCTP" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToMonitorCreateMap() (map[string]any, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The Pool to Monitor. + PoolID string `json:"pool_id,omitempty"` + + // The type of probe, which is PING, TCP, HTTP, or HTTPS, that is + // sent by the load balancer to verify the member state. + Type string `json:"type" required:"true"` + + // The time, in seconds, between sending probes to members. + Delay int `json:"delay" required:"true"` + + // Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout" required:"true"` + + // Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries" required:"true"` + + // Number of permissible ping failures befor changing the member's + // status to ERROR. Must be a number between 1 and 10. + MaxRetriesDown int `json:"max_retries_down,omitempty"` + + // URI path that will be accessed if Monitor type is HTTP or HTTPS. + URLPath string `json:"url_path,omitempty"` + + // The HTTP method used for requests by the Monitor. If this attribute + // is not specified, it defaults to "GET". Required for HTTP(S) types. + HTTPMethod string `json:"http_method,omitempty"` + + // The HTTP version. One of 1.0 or 1.1. The default is 1.0. New in + // version 2.10. + HTTPVersion string `json:"http_version,omitempty"` + + // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify + // a single status like "200", a range like "200-202", or a combination like + // "200-202, 401". + ExpectedCodes string `json:"expected_codes,omitempty"` + + // TenantID is the UUID of the project who owns the Monitor. + // Only administrative users can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Monitor. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // The Name of the Monitor. + Name string `json:"name,omitempty"` + + // The administrative state of the Monitor. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // The domain name, which be injected into the HTTP Host Header to the + // backend server for HTTP health check. New in version 2.10 + DomainName string `json:"domain_name,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags []string `json:"tags,omitempty"` +} + +// ToMonitorCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToMonitorCreateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "healthmonitor") + if err != nil { + return nil, err + } + + if v, ok := b["healthmonitor"]; ok { + if m, ok := v.(map[string]any); ok { + if v, ok := m["http_version"]; ok { + if v, ok := v.(string); ok { + m["http_version"], err = strconv.ParseFloat(v, 64) + if err != nil { + return nil, err + } + } + } + } + } + + return b, nil +} + +/* +Create is an operation which provisions a new Health Monitor. There are +different types of Monitor you can provision: PING, TCP or HTTP(S). Below +are examples of how to create each one. + +Here is an example config struct to use when creating a PING or TCP Monitor: + +CreateOpts{Type: TypePING, Delay: 20, Timeout: 10, MaxRetries: 3} +CreateOpts{Type: TypeTCP, Delay: 20, Timeout: 10, MaxRetries: 3} + +Here is an example config struct to use when creating a HTTP(S) Monitor: + +CreateOpts{Type: TypeHTTP, Delay: 20, Timeout: 10, MaxRetries: 3, +HttpMethod: "HEAD", ExpectedCodes: "200", PoolID: "2c946bfc-1804-43ab-a2ff-58f6a762b505"} +*/ +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToMonitorCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular Health Monitor based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToMonitorUpdateMap() (map[string]any, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // The time, in seconds, between sending probes to members. + Delay int `json:"delay,omitempty"` + + // Maximum number of seconds for a Monitor to wait for a ping reply + // before it times out. The value must be less than the delay value. + Timeout int `json:"timeout,omitempty"` + + // Number of permissible ping failures before changing the member's + // status to INACTIVE. Must be a number between 1 and 10. + MaxRetries int `json:"max_retries,omitempty"` + + // Number of permissible ping failures befor changing the member's + // status to ERROR. Must be a number between 1 and 10. + MaxRetriesDown int `json:"max_retries_down,omitempty"` + + // URI path that will be accessed if Monitor type is HTTP or HTTPS. + // Required for HTTP(S) types. + URLPath string `json:"url_path,omitempty"` + + // The HTTP method used for requests by the Monitor. If this attribute + // is not specified, it defaults to "GET". Required for HTTP(S) types. + HTTPMethod string `json:"http_method,omitempty"` + + // The HTTP version. One of 1.0 or 1.1. The default is 1.0. New in + // version 2.10. + HTTPVersion *string `json:"http_version,omitempty"` + + // Expected HTTP codes for a passing HTTP(S) Monitor. You can either specify + // a single status like "200", or a range like "200-202". Required for HTTP(S) + // types. + ExpectedCodes string `json:"expected_codes,omitempty"` + + // The Name of the Monitor. + Name *string `json:"name,omitempty"` + + // The domain name, which be injected into the HTTP Host Header to the + // backend server for HTTP health check. New in version 2.10 + DomainName *string `json:"domain_name,omitempty"` + + // The administrative state of the Monitor. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags []string `json:"tags,omitempty"` +} + +// ToMonitorUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToMonitorUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "healthmonitor") + if err != nil { + return nil, err + } + + if v, ok := b["healthmonitor"]; ok { + if m, ok := v.(map[string]any); ok { + if v, ok := m["http_version"]; ok { + if v, ok := v.(string); ok { + m["http_version"], err = strconv.ParseFloat(v, 64) + if err != nil { + return nil, err + } + } + } + } + } + + return b, nil +} + +// Update is an operation which modifies the attributes of the specified +// Monitor. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToMonitorUpdateMap() + if err != nil { + r.Err = err + return + } + + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular Monitor based on its unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/results.go new file mode 100644 index 000000000..6e8563faa --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/results.go @@ -0,0 +1,200 @@ +package monitors + +import ( + "encoding/json" + "strconv" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type PoolID struct { + ID string `json:"id"` +} + +// Monitor represents a load balancer health monitor. A health monitor is used +// to determine whether or not back-end members of the VIP's pool are usable +// for processing a request. A pool can have several health monitors associated +// with it. There are different types of health monitors supported: +// +// PING: used to ping the members using ICMP. +// TCP: used to connect to the members using TCP. +// HTTP: used to send an HTTP request to the member. +// HTTPS: used to send a secure HTTP request to the member. +// TLS-HELLO: used to send TLS-HELLO request to the member. +// UDP-CONNECT: used to send UDP-CONNECT request to the member. +// SCTP: used to send SCTP request to the member. +// +// When a pool has several monitors associated with it, each member of the pool +// is monitored by all these monitors. If any monitor declares the member as +// unhealthy, then the member status is changed to INACTIVE and the member +// won't participate in its pool's load balancing. In other words, ALL monitors +// must declare the member to be healthy for it to stay ACTIVE. +type Monitor struct { + // The unique ID for the Monitor. + ID string `json:"id"` + + // The Name of the Monitor. + Name string `json:"name"` + + // The owner of the Monitor. + ProjectID string `json:"project_id"` + + // The type of probe sent by the load balancer to verify the member state, + // which is PING, TCP, HTTP, HTTPS, TLS-HELLO, UDP-CONNECT or SCTP. + Type string `json:"type"` + + // The time, in seconds, between sending probes to members. + Delay int `json:"delay"` + + // The maximum number of seconds for a monitor to wait for a connection to be + // established before it times out. This value must be less than the delay + // value. + Timeout int `json:"timeout"` + + // Number of allowed connection failures before changing the status of the + // member to INACTIVE. A valid value is from 1 to 10. + MaxRetries int `json:"max_retries"` + + // Number of allowed connection failures before changing the status of the + // member to Error. A valid value is from 1 to 10. + MaxRetriesDown int `json:"max_retries_down"` + + // The HTTP method that the monitor uses for requests. + HTTPMethod string `json:"http_method"` + + // The HTTP version that the monitor uses for requests. + HTTPVersion string `json:"-"` + + // The HTTP path of the request sent by the monitor to test the health of a + // member. Must be a string beginning with a forward slash (/). + URLPath string `json:"url_path" ` + + // Expected HTTP codes for a passing HTTP(S) monitor. + ExpectedCodes string `json:"expected_codes"` + + // The HTTP host header that the monitor uses for requests. + DomainName string `json:"domain_name"` + + // The administrative state of the health monitor, which is up (true) or + // down (false). + AdminStateUp bool `json:"admin_state_up"` + + // The status of the health monitor. Indicates whether the health monitor is + // operational. + Status string `json:"status"` + + // List of pools that are associated with the health monitor. + Pools []PoolID `json:"pools"` + + // The provisioning status of the Monitor. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the monitor. + OperatingStatus string `json:"operating_status"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. New in version 2.5 + Tags []string `json:"tags"` +} + +func (r *Monitor) UnmarshalJSON(b []byte) error { + type tmp Monitor + var s struct { + tmp + HTTPVersion float64 `json:"http_version"` + } + + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + + *r = Monitor(s.tmp) + if s.HTTPVersion != 0 { + r.HTTPVersion = strconv.FormatFloat(s.HTTPVersion, 'f', 1, 64) + } + + return nil +} + +// MonitorPage is the page returned by a pager when traversing over a +// collection of health monitors. +type MonitorPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of monitors has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MonitorPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"healthmonitors_links"` + } + + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MonitorPage struct is empty. +func (r MonitorPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractMonitors(r) + return len(is) == 0, err +} + +// ExtractMonitors accepts a Page struct, specifically a MonitorPage struct, +// and extracts the elements into a slice of Monitor structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMonitors(r pagination.Page) ([]Monitor, error) { + var s struct { + Monitors []Monitor `json:"healthmonitors"` + } + err := (r.(MonitorPage)).ExtractInto(&s) + return s.Monitors, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a monitor. +func (r commonResult) Extract() (*Monitor, error) { + var s struct { + Monitor *Monitor `json:"healthmonitor"` + } + err := r.ExtractInto(&s) + return s.Monitor, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Monitor. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Monitor. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Monitor. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the result succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/urls.go new file mode 100644 index 000000000..d5723a305 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors/urls.go @@ -0,0 +1,16 @@ +package monitors + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "lbaas" + resourcePath = "healthmonitors" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/doc.go new file mode 100644 index 000000000..f5156d661 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/doc.go @@ -0,0 +1,157 @@ +/* +Package pools provides information and interaction with Pools and +Members of the LBaaS v2 extension for the OpenStack Networking service. + +Example to List Pools + + listOpts := pools.ListOpts{ + LoadbalancerID: "c79a4468-d788-410c-bf79-9a8ef6354852", + } + + allPages, err := pools.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allPools, err := pools.ExtractMonitors(allPages) + if err != nil { + panic(err) + } + + for _, pools := range allPools { + fmt.Printf("%+v\n", pool) + } + +Example to Create a Pool + + createOpts := pools.CreateOpts{ + LBMethod: pools.LBMethodRoundRobin, + Protocol: "HTTP", + Name: "Example pool", + Tags: []string{"test"}, + LoadbalancerID: "79e05663-7f03-45d2-a092-8b94062f22ab", + } + + pool, err := pools.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Pool + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + newTags := []string{"prod"} + updateOpts := pools.UpdateOpts{ + Name: "new-name", + Tags: &newTags, + } + + pool, err := pools.Update(context.TODO(), networkClient, poolID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Pool + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + err := pools.Delete(context.TODO(), networkClient, poolID).ExtractErr() + if err != nil { + panic(err) + } + +Example to List Pool Members + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + listOpts := pools.ListMemberOpts{ + ProtocolPort: 80, + } + + allPages, err := pools.ListMembers(networkClient, poolID, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allMembers, err := pools.ExtractMembers(allPages) + if err != nil { + panic(err) + } + + for _, member := allMembers { + fmt.Printf("%+v\n", member) + } + +Example to Create a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + weight := 10 + createOpts := pools.CreateMemberOpts{ + Name: "db", + SubnetID: "1981f108-3c48-48d2-b908-30f7d28532c9", + Address: "10.0.2.11", + ProtocolPort: 80, + Weight: &weight, + } + + member, err := pools.CreateMember(context.TODO(), networkClient, poolID, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + memberID := "64dba99f-8af8-4200-8882-e32a0660f23e" + + weight := 4 + updateOpts := pools.UpdateMemberOpts{ + Name: "new-name", + Weight: &weight, + } + + member, err := pools.UpdateMember(context.TODO(), networkClient, poolID, memberID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Member + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + memberID := "64dba99f-8af8-4200-8882-e32a0660f23e" + + err := pools.DeleteMember(context.TODO(), networkClient, poolID, memberID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Update Members: + + poolID := "d67d56a6-4a86-4688-a282-f46444705c64" + + weight_1 := 20 + member1 := pools.BatchUpdateMemberOpts{ + Address: "192.0.2.16", + ProtocolPort: 80, + Name: "web-server-1", + SubnetID: "bbb35f84-35cc-4b2f-84c2-a6a29bba68aa", + Weight: &weight_1, + } + + weight_2 := 10 + member2 := pools.BatchUpdateMemberOpts{ + Address: "192.0.2.17", + ProtocolPort: 80, + Name: "web-server-2", + Weight: &weight_2, + SubnetID: "bbb35f84-35cc-4b2f-84c2-a6a29bba68aa", + } + members := []pools.BatchUpdateMemberOpts{member1, member2} + + err := pools.BatchUpdateMembers(context.TODO(), networkClient, poolID, members).ExtractErr() + if err != nil { + panic(err) + } +*/ +package pools diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/requests.go new file mode 100644 index 000000000..344368637 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/requests.go @@ -0,0 +1,597 @@ +package pools + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Type TLSVersion represents a tls version +type TLSVersion string + +const ( + TLSVersionSSLv3 TLSVersion = "SSLv3" + TLSVersionTLSv1 TLSVersion = "TLSv1" + TLSVersionTLSv1_1 TLSVersion = "TLSv1.1" + TLSVersionTLSv1_2 TLSVersion = "TLSv1.2" + TLSVersionTLSv1_3 TLSVersion = "TLSv1.3" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPoolListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Pool attributes you want to see returned. SortKey allows you to +// sort by a particular Pool attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + LBMethod string `q:"lb_algorithm"` + Protocol string `q:"protocol"` + ProjectID string `q:"project_id"` + AdminStateUp *bool `q:"admin_state_up"` + Name string `q:"name"` + ID string `q:"id"` + LoadbalancerID string `q:"loadbalancer_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags []string `q:"tags"` +} + +// ToPoolListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPoolListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// pools. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those pools that are owned by the +// project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToPoolListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PoolPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type LBMethod string +type Protocol string + +// Supported attributes for create/update operations. +const ( + LBMethodRoundRobin LBMethod = "ROUND_ROBIN" + LBMethodLeastConnections LBMethod = "LEAST_CONNECTIONS" + LBMethodSourceIp LBMethod = "SOURCE_IP" + LBMethodSourceIpPort LBMethod = "SOURCE_IP_PORT" + + ProtocolTCP Protocol = "TCP" + ProtocolUDP Protocol = "UDP" + ProtocolPROXY Protocol = "PROXY" + ProtocolHTTP Protocol = "HTTP" + ProtocolHTTPS Protocol = "HTTPS" + // Protocol PROXYV2 requires octavia microversion 2.22 + ProtocolPROXYV2 Protocol = "PROXYV2" + // Protocol SCTP requires octavia microversion 2.23 + ProtocolSCTP Protocol = "SCTP" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPoolCreateMap() (map[string]any, error) +} + +// CreateOpts is the common options struct used in this package's Create +// operation. +type CreateOpts struct { + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections, + // LBMethodSourceIp and LBMethodSourceIpPort as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm" required:"true"` + + // The protocol used by the pool members, you can use either + // ProtocolTCP, ProtocolUDP, ProtocolPROXY, ProtocolHTTP, ProtocolHTTPS, + // ProtocolSCTP or ProtocolPROXYV2. + Protocol Protocol `json:"protocol" required:"true"` + + // The Loadbalancer on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + LoadbalancerID string `json:"loadbalancer_id,omitempty"` + + // The Listener on which the members of the pool will be associated with. + // Note: one of LoadbalancerID or ListenerID must be provided. + ListenerID string `json:"listener_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Pool. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // Name of the pool. + Name string `json:"name,omitempty"` + + // Human-readable description for the pool. + Description string `json:"description,omitempty"` + + // Persistence is the session persistence of the pool. + // Omit this field to prevent session persistence. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + + // A list of ALPN protocols. Available protocols: http/1.0, http/1.1, + // h2. Available from microversion 2.24. + ALPNProtocols []string `json:"alpn_protocols,omitempty"` + + // The reference of the key manager service secret containing a PEM + // format CA certificate bundle for tls_enabled pools. Available from + // microversion 2.8. + CATLSContainerRef string `json:"ca_tls_container_ref,omitempty"` + + // The reference of the key manager service secret containing a PEM + // format CA revocation list file for tls_enabled pools. Available from + // microversion 2.8. + CRLContainerRef string `json:"crl_container_ref,omitempty"` + + // When true connections to backend member servers will use TLS + // encryption. Default is false. Available from microversion 2.8. + TLSEnabled bool `json:"tls_enabled,omitempty"` + + // List of ciphers in OpenSSL format (colon-separated). Available from + // microversion 2.15. + TLSCiphers string `json:"tls_ciphers,omitempty"` + + // The reference to the key manager service secret containing a PKCS12 + // format certificate/key bundle for tls_enabled pools for TLS client + // authentication to the member servers. Available from microversion 2.8. + TLSContainerRef string `json:"tls_container_ref,omitempty"` + + // A list of TLS protocol versions. Available versions: SSLv3, TLSv1, + // TLSv1.1, TLSv1.2, TLSv1.3. Available from microversion 2.17. + TLSVersions []TLSVersion `json:"tls_versions,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Members is a slice of CreateMemberOpts which allows a set of + // members to be created at the same time the pool is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + Members []CreateMemberOpts `json:"members,omitempty"` + + // Monitor is an instance of monitors.CreateOpts which allows a monitor + // to be created at the same time the pool is created. + // + // This is only possible to use when creating a fully populated + // Loadbalancer. + Monitor monitors.CreateOptsBuilder `json:"healthmonitor,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags []string `json:"tags,omitempty"` +} + +// ToPoolCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPoolCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "pool") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// load balancer pool. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPoolCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular pool based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToPoolUpdateMap() (map[string]any, error) +} + +// UpdateOpts is the common options struct used in this package's Update +// operation. +type UpdateOpts struct { + // Name of the pool. + Name *string `json:"name,omitempty"` + + // Human-readable description for the pool. + Description *string `json:"description,omitempty"` + + // The algorithm used to distribute load between the members of the pool. The + // current specification supports LBMethodRoundRobin, LBMethodLeastConnections, + // LBMethodSourceIp and LBMethodSourceIpPort as valid values for this attribute. + LBMethod LBMethod `json:"lb_algorithm,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Persistence is the session persistence of the pool. + Persistence *SessionPersistence `json:"session_persistence,omitempty"` + + // A list of ALPN protocols. Available protocols: http/1.0, http/1.1, + // h2. Available from microversion 2.24. + ALPNProtocols *[]string `json:"alpn_protocols,omitempty"` + + // The reference of the key manager service secret containing a PEM + // format CA certificate bundle for tls_enabled pools. Available from + // microversion 2.8. + CATLSContainerRef *string `json:"ca_tls_container_ref,omitempty"` + + // The reference of the key manager service secret containing a PEM + // format CA revocation list file for tls_enabled pools. Available from + // microversion 2.8. + CRLContainerRef *string `json:"crl_container_ref,omitempty"` + + // When true connections to backend member servers will use TLS + // encryption. Default is false. Available from microversion 2.8. + TLSEnabled *bool `json:"tls_enabled,omitempty"` + + // List of ciphers in OpenSSL format (colon-separated). Available from + // microversion 2.15. + TLSCiphers *string `json:"tls_ciphers,omitempty"` + + // The reference to the key manager service secret containing a PKCS12 + // format certificate/key bundle for tls_enabled pools for TLS client + // authentication to the member servers. Available from microversion 2.8. + TLSContainerRef *string `json:"tls_container_ref,omitempty"` + + // A list of TLS protocol versions. Available versions: SSLv3, TLSv1, + // TLSv1.1, TLSv1.2, TLSv1.3. Available from microversion 2.17. + TLSVersions *[]TLSVersion `json:"tls_versions,omitempty"` + + // Tags is a set of resource tags. New in version 2.5 + Tags *[]string `json:"tags,omitempty"` +} + +// ToPoolUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPoolUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "pool") + if err != nil { + return nil, err + } + + m := b["pool"].(map[string]any) + + // allow to unset session_persistence on empty SessionPersistence struct + if opts.Persistence != nil && *opts.Persistence == (SessionPersistence{}) { + m["session_persistence"] = nil + } + + // allow to unset alpn_protocols on empty slice + if opts.ALPNProtocols != nil && len(*opts.ALPNProtocols) == 0 { + m["alpn_protocols"] = nil + } + + // allow to unset tls_versions on empty slice + if opts.TLSVersions != nil && len(*opts.TLSVersions) == 0 { + m["tls_versions"] = nil + } + + return b, nil +} + +// Update allows pools to be updated. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPoolUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular pool based on its unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListMemberOptsBuilder allows extensions to add additional parameters to the +// ListMembers request. +type ListMembersOptsBuilder interface { + ToMembersListQuery() (string, error) +} + +// ListMembersOpts allows the filtering and sorting of paginated collections +// through the API. Filtering is achieved by passing in struct field values +// that map to the Member attributes you want to see returned. SortKey allows +// you to sort by a particular Member attribute. SortDir sets the direction, +// and is either `asc' or `desc'. Marker and Limit are used for pagination. +type ListMembersOpts struct { + Name string `q:"name"` + Weight int `q:"weight"` + AdminStateUp *bool `q:"admin_state_up"` + ProjectID string `q:"project_id"` + Address string `q:"address"` + ProtocolPort int `q:"protocol_port"` + ID string `q:"id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` +} + +// ToMemberListQuery formats a ListOpts into a query string. +func (opts ListMembersOpts) ToMembersListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListMembers returns a Pager which allows you to iterate over a collection of +// members. It accepts a ListMembersOptsBuilder, which allows you to filter and +// sort the returned collection for greater efficiency. +// +// Default policy settings return only those members that are owned by the +// project who submits the request, unless an admin user submits the request. +func ListMembers(c *gophercloud.ServiceClient, poolID string, opts ListMembersOptsBuilder) pagination.Pager { + url := memberRootURL(c, poolID) + if opts != nil { + query, err := opts.ToMembersListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return MemberPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateMemberOptsBuilder allows extensions to add additional parameters to the +// CreateMember request. +type CreateMemberOptsBuilder interface { + ToMemberCreateMap() (map[string]any, error) +} + +// CreateMemberOpts is the common options struct used in this package's CreateMember +// operation. +type CreateMemberOpts struct { + // The IP address of the member to receive traffic from the load balancer. + Address string `json:"address" required:"true"` + + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + + // Name of the Member. + Name string `json:"name,omitempty"` + + // ProjectID is the UUID of the project who owns the Member. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // A positive integer value that indicates the relative portion of traffic + // that this member should receive from the pool. For example, a member with + // a weight of 10 receives five times as much traffic as a member with a + // weight of 2. + Weight *int `json:"weight,omitempty"` + + // If you omit this parameter, LBaaS uses the vip_subnet_id parameter value + // for the subnet UUID. + SubnetID string `json:"subnet_id,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Is the member a backup? Backup members only receive traffic when all + // non-backup members are down. + // Requires microversion 2.1 or later. + Backup *bool `json:"backup,omitempty"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress string `json:"monitor_address,omitempty"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort *int `json:"monitor_port,omitempty"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToMemberCreateMap builds a request body from CreateMemberOpts. +func (opts CreateMemberOpts) ToMemberCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// CreateMember will create and associate a Member with a particular Pool. +func CreateMember(ctx context.Context, c *gophercloud.ServiceClient, poolID string, opts CreateMemberOptsBuilder) (r CreateMemberResult) { + b, err := opts.ToMemberCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, memberRootURL(c, poolID), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// GetMember retrieves a particular Pool Member based on its unique ID. +func GetMember(ctx context.Context, c *gophercloud.ServiceClient, poolID string, memberID string) (r GetMemberResult) { + resp, err := c.Get(ctx, memberResourceURL(c, poolID, memberID), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateMemberOptsBuilder allows extensions to add additional parameters to the +// List request. +type UpdateMemberOptsBuilder interface { + ToMemberUpdateMap() (map[string]any, error) +} + +// UpdateMemberOpts is the common options struct used in this package's Update +// operation. +type UpdateMemberOpts struct { + // Name of the Member. + Name *string `json:"name,omitempty"` + + // A positive integer value that indicates the relative portion of traffic + // that this member should receive from the pool. For example, a member with + // a weight of 10 receives five times as much traffic as a member with a + // weight of 2. + Weight *int `json:"weight,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Is the member a backup? Backup members only receive traffic when all + // non-backup members are down. + // Requires microversion 2.1 or later. + Backup *bool `json:"backup,omitempty"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress *string `json:"monitor_address,omitempty"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort *int `json:"monitor_port,omitempty"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToMemberUpdateMap builds a request body from UpdateMemberOpts. +func (opts UpdateMemberOpts) ToMemberUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "member") +} + +// Update allows Member to be updated. +func UpdateMember(ctx context.Context, c *gophercloud.ServiceClient, poolID string, memberID string, opts UpdateMemberOptsBuilder) (r UpdateMemberResult) { + b, err := opts.ToMemberUpdateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, memberResourceURL(c, poolID, memberID), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200, 201, 202}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// BatchUpdateMemberOptsBuilder allows extensions to add additional parameters to the BatchUpdateMembers request. +type BatchUpdateMemberOptsBuilder interface { + ToBatchMemberUpdateMap() (map[string]any, error) +} + +// BatchUpdateMemberOpts is the common options struct used in this package's BatchUpdateMembers +// operation. +type BatchUpdateMemberOpts struct { + // The IP address of the member to receive traffic from the load balancer. + Address string `json:"address" required:"true"` + + // The port on which to listen for client traffic. + ProtocolPort int `json:"protocol_port" required:"true"` + + // Name of the Member. + Name *string `json:"name,omitempty"` + + // ProjectID is the UUID of the project who owns the Member. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // A positive integer value that indicates the relative portion of traffic + // that this member should receive from the pool. For example, a member with + // a weight of 10 receives five times as much traffic as a member with a + // weight of 2. + Weight *int `json:"weight,omitempty"` + + // If you omit this parameter, LBaaS uses the vip_subnet_id parameter value + // for the subnet UUID. + SubnetID *string `json:"subnet_id,omitempty"` + + // The administrative state of the Pool. A valid value is true (UP) + // or false (DOWN). + AdminStateUp *bool `json:"admin_state_up,omitempty"` + + // Is the member a backup? Backup members only receive traffic when all + // non-backup members are down. + // Requires microversion 2.1 or later. + Backup *bool `json:"backup,omitempty"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress *string `json:"monitor_address,omitempty"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort *int `json:"monitor_port,omitempty"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags,omitempty"` +} + +// ToBatchMemberUpdateMap builds a request body from BatchUpdateMemberOpts. +func (opts BatchUpdateMemberOpts) ToBatchMemberUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "") + if err != nil { + return nil, err + } + + if b["subnet_id"] == "" { + b["subnet_id"] = nil + } + + return b, nil +} + +// BatchUpdateMembers updates the pool members in batch +func BatchUpdateMembers[T BatchUpdateMemberOptsBuilder](ctx context.Context, c *gophercloud.ServiceClient, poolID string, opts []T) (r UpdateMembersResult) { + members := []map[string]any{} + for _, opt := range opts { + b, err := opt.ToBatchMemberUpdateMap() + if err != nil { + r.Err = err + return + } + members = append(members, b) + } + + b := map[string]any{"members": members} + + resp, err := c.Put(ctx, memberRootURL(c, poolID), b, nil, &gophercloud.RequestOpts{OkCodes: []int{202}}) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteMember will remove and disassociate a Member from a particular Pool. +func DeleteMember(ctx context.Context, c *gophercloud.ServiceClient, poolID string, memberID string) (r DeleteMemberResult) { + resp, err := c.Delete(ctx, memberResourceURL(c, poolID, memberID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/results.go new file mode 100644 index 000000000..bff336ea4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/results.go @@ -0,0 +1,380 @@ +package pools + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// SessionPersistence represents the session persistence feature of the load +// balancing service. It attempts to force connections or requests in the same +// session to be processed by the same member as long as it is ative. Three +// types of persistence are supported: +// +// SOURCE_IP: With this mode, all connections originating from the same source +// +// IP address, will be handled by the same Member of the Pool. +// +// HTTP_COOKIE: With this persistence mode, the load balancing function will +// +// create a cookie on the first request from a client. Subsequent +// requests containing the same cookie value will be handled by +// the same Member of the Pool. +// +// APP_COOKIE: With this persistence mode, the load balancing function will +// +// rely on a cookie established by the backend application. All +// requests carrying the same cookie value will be handled by the +// same Member of the Pool. +type SessionPersistence struct { + // The type of persistence mode. + Type string `json:"type"` + + // Name of cookie if persistence mode is set appropriately. + CookieName string `json:"cookie_name,omitempty"` +} + +// LoadBalancerID represents a load balancer. +type LoadBalancerID struct { + ID string `json:"id"` +} + +// ListenerID represents a listener. +type ListenerID struct { + ID string `json:"id"` +} + +// Pool represents a logical set of devices, such as web servers, that you +// group together to receive and process traffic. The load balancing function +// chooses a Member of the Pool according to the configured load balancing +// method to handle the new requests or connections received on the VIP address. +type Pool struct { + // The load-balancer algorithm, which is round-robin, least-connections, and + // so on. This value, which must be supported, is dependent on the provider. + // Round-robin must be supported. + LBMethod string `json:"lb_algorithm"` + + // The protocol of the Pool, which is TCP, HTTP, or HTTPS. + Protocol string `json:"protocol"` + + // Description for the Pool. + Description string `json:"description"` + + // A list of listeners objects IDs. + Listeners []ListenerID `json:"listeners"` //[]map[string]any + + // A list of member objects IDs. + Members []Member `json:"members"` + + // The ID of associated health monitor. + MonitorID string `json:"healthmonitor_id"` + + // The network on which the members of the Pool will be located. Only members + // that are on this network can be added to the Pool. + SubnetID string `json:"subnet_id"` + + // Owner of the Pool. + ProjectID string `json:"project_id"` + + // The administrative state of the Pool, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // Pool name. Does not have to be unique. + Name string `json:"name"` + + // The unique ID for the Pool. + ID string `json:"id"` + + // A list of load balancer objects IDs. + Loadbalancers []LoadBalancerID `json:"loadbalancers"` + + // Indicates whether connections in the same session will be processed by the + // same Pool member or not. + Persistence SessionPersistence `json:"session_persistence"` + + // A list of ALPN protocols. Available protocols: http/1.0, http/1.1, + // h2. Available from microversion 2.24. + ALPNProtocols []string `json:"alpn_protocols"` + + // The reference of the key manager service secret containing a PEM + // format CA certificate bundle for tls_enabled pools. Available from + // microversion 2.8. + CATLSContainerRef string `json:"ca_tls_container_ref"` + + // The reference of the key manager service secret containing a PEM + // format CA revocation list file for tls_enabled pools. Available from + // microversion 2.8. + CRLContainerRef string `json:"crl_container_ref"` + + // When true connections to backend member servers will use TLS + // encryption. Default is false. Available from microversion 2.8. + TLSEnabled bool `json:"tls_enabled"` + + // List of ciphers in OpenSSL format (colon-separated). Available from + // microversion 2.15. + TLSCiphers string `json:"tls_ciphers"` + + // The reference to the key manager service secret containing a PKCS12 + // format certificate/key bundle for tls_enabled pools for TLS client + // authentication to the member servers. Available from microversion 2.8. + TLSContainerRef string `json:"tls_container_ref"` + + // A list of TLS protocol versions. Available versions: SSLv3, TLSv1, + // TLSv1.1, TLSv1.2, TLSv1.3. Available from microversion 2.17. + TLSVersions []string `json:"tls_versions"` + + // The load balancer provider. + Provider string `json:"provider"` + + // The Monitor associated with this Pool. + Monitor monitors.Monitor `json:"healthmonitor"` + + // The provisioning status of the pool. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // The operating status of the pool. + OperatingStatus string `json:"operating_status"` + + // Tags is a list of resource tags. Tags are arbitrarily defined strings + // attached to the resource. New in version 2.5 + Tags []string `json:"tags"` +} + +// PoolPage is the page returned by a pager when traversing over a +// collection of pools. +type PoolPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of pools has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PoolPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"pools_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PoolPage struct is empty. +func (r PoolPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractPools(r) + return len(is) == 0, err +} + +// ExtractPools accepts a Page struct, specifically a PoolPage struct, +// and extracts the elements into a slice of Pool structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPools(r pagination.Page) ([]Pool, error) { + var s struct { + Pools []Pool `json:"pools"` + } + err := (r.(PoolPage)).ExtractInto(&s) + return s.Pools, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a pool. +func (r commonResult) Extract() (*Pool, error) { + var s struct { + Pool *Pool `json:"pool"` + } + err := r.ExtractInto(&s) + return s.Pool, err +} + +// CreateResult represents the result of a Create operation. Call its Extract +// method to interpret the result as a Pool. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a Get operation. Call its Extract +// method to interpret the result as a Pool. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an Update operation. Call its Extract +// method to interpret the result as a Pool. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a Delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Member represents the application running on a backend server. +type Member struct { + // Name of the Member. + Name string `json:"name"` + + // Weight of Member. + Weight int `json:"weight"` + + // The administrative state of the member, which is up (true) or down (false). + AdminStateUp bool `json:"admin_state_up"` + + // Owner of the Member. + ProjectID string `json:"project_id"` + + // Parameter value for the subnet UUID. + SubnetID string `json:"subnet_id"` + + // The Pool to which the Member belongs. + PoolID string `json:"pool_id"` + + // The IP address of the Member. + Address string `json:"address"` + + // The port on which the application is hosted. + ProtocolPort int `json:"protocol_port"` + + // The unique ID for the Member. + ID string `json:"id"` + + // The provisioning status of the pool. + // This value is ACTIVE, PENDING_* or ERROR. + ProvisioningStatus string `json:"provisioning_status"` + + // DateTime when the member was created + CreatedAt time.Time `json:"-"` + + // DateTime when the member was updated + UpdatedAt time.Time `json:"-"` + + // The operating status of the member + OperatingStatus string `json:"operating_status"` + + // Is the member a backup? Backup members only receive traffic when all non-backup members are down. + Backup bool `json:"backup"` + + // An alternate IP address used for health monitoring a backend member. + MonitorAddress string `json:"monitor_address"` + + // An alternate protocol port used for health monitoring a backend member. + MonitorPort int `json:"monitor_port"` + + // A list of simple strings assigned to the resource. + // Requires microversion 2.5 or later. + Tags []string `json:"tags"` +} + +// MemberPage is the page returned by a pager when traversing over a +// collection of Members in a Pool. +type MemberPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of members has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r MemberPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"members_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a MemberPage struct is empty. +func (r MemberPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractMembers(r) + return len(is) == 0, err +} + +// ExtractMembers accepts a Page struct, specifically a MemberPage struct, +// and extracts the elements into a slice of Members structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractMembers(r pagination.Page) ([]Member, error) { + var s struct { + Members []Member `json:"members"` + } + err := (r.(MemberPage)).ExtractInto(&s) + return s.Members, err +} + +type commonMemberResult struct { + gophercloud.Result +} + +func (r *Member) UnmarshalJSON(b []byte) error { + type tmp Member + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Member(s.tmp) + r.CreatedAt = time.Time(s.CreatedAt) + r.UpdatedAt = time.Time(s.UpdatedAt) + return nil +} + +// ExtractMember is a function that accepts a result and extracts a member. +func (r commonMemberResult) Extract() (*Member, error) { + var s struct { + Member *Member `json:"member"` + } + err := r.ExtractInto(&s) + return s.Member, err +} + +// CreateMemberResult represents the result of a CreateMember operation. +// Call its Extract method to interpret it as a Member. +type CreateMemberResult struct { + commonMemberResult +} + +// GetMemberResult represents the result of a GetMember operation. +// Call its Extract method to interpret it as a Member. +type GetMemberResult struct { + commonMemberResult +} + +// UpdateMemberResult represents the result of an UpdateMember operation. +// Call its Extract method to interpret it as a Member. +type UpdateMemberResult struct { + commonMemberResult +} + +// UpdateMembersResult represents the result of an UpdateMembers operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type UpdateMembersResult struct { + gophercloud.ErrResult +} + +// DeleteMemberResult represents the result of a DeleteMember operation. +// Call its ExtractErr method to determine if the request succeeded or failed. +type DeleteMemberResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/urls.go new file mode 100644 index 000000000..a362f1b95 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools/urls.go @@ -0,0 +1,25 @@ +package pools + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "lbaas" + resourcePath = "pools" + memberPath = "members" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, resourcePath, id) +} + +func memberRootURL(c *gophercloud.ServiceClient, poolId string) string { + return c.ServiceURL(rootPath, resourcePath, poolId, memberPath) +} + +func memberResourceURL(c *gophercloud.ServiceClient, poolID string, memberID string) string { + return c.ServiceURL(rootPath, resourcePath, poolID, memberPath, memberID) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/doc.go new file mode 100644 index 000000000..af799758d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/doc.go @@ -0,0 +1,21 @@ +/* +Package providers provides information about the supported providers +at OpenStack Octavia Load Balancing service. + +Example to List Providers + + allPages, err := providers.List(lbClient).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allProviders, err := providers.ExtractProviders(allPages) + if err != nil { + panic(err) + } + + for _, p := range allProviders { + fmt.Printf("%+v\n", p) + } +*/ +package providers diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/requests.go new file mode 100644 index 000000000..8b8e07c96 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/requests.go @@ -0,0 +1,44 @@ +package providers + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToProviderListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the Provider attributes you want to see returned. +type ListOpts struct { + Fields []string `q:"fields"` +} + +// ToProviderListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToProviderListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// providers. +// +// Default policy settings return only those providers that are owned by +// the project who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToProviderListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return ProviderPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/results.go new file mode 100644 index 000000000..ec374faee --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/results.go @@ -0,0 +1,75 @@ +package providers + +import ( + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Provider is the Octavia driver that implements the load balancing mechanism +type Provider struct { + // Human-readable description for the Loadbalancer. + Description string `json:"description"` + + // Human-readable name for the Provider. + Name string `json:"name"` +} + +// ProviderPage is the page returned by a pager when traversing over a +// collection of providers. +type ProviderPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of providers has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r ProviderPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"providers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a ProviderPage struct is empty. +func (r ProviderPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractProviders(r) + return len(is) == 0, err +} + +// ExtractProviders accepts a Page struct, specifically a ProviderPage +// struct, and extracts the elements into a slice of Provider structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractProviders(r pagination.Page) ([]Provider, error) { + var s struct { + Providers []Provider `json:"providers"` + } + err := (r.(ProviderPage)).ExtractInto(&s) + return s.Providers, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a provider. +func (r commonResult) Extract() (*Provider, error) { + var s struct { + Provider *Provider `json:"provider"` + } + err := r.ExtractInto(&s) + return s.Provider, err +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Provider. +type GetResult struct { + commonResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/urls.go new file mode 100644 index 000000000..bd74a8425 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers/urls.go @@ -0,0 +1,12 @@ +package providers + +import "github.com/gophercloud/gophercloud/v2" + +const ( + rootPath = "lbaas" + resourcePath = "providers" +) + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath, resourcePath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/doc.go new file mode 100644 index 000000000..b511a0503 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/doc.go @@ -0,0 +1,37 @@ +/* +Package attributestags manages Tags on Resources created by the OpenStack Neutron Service. + +This enables tagging via a standard interface for resources types which support it. + +See https://developer.openstack.org/api-ref/network/v2/#standard-attributes-tag-extension for more information on the underlying API. + +Example to ReplaceAll Resource Tags + + network, err := networks.Create(context.TODO(), client, createOpts).Extract() + + tagReplaceAllOpts := attributestags.ReplaceAllOpts{ + Tags: []string{"abc", "123"}, + } + attributestags.ReplaceAll(context.TODO(), client, "networks", network.ID, tagReplaceAllOpts) + +Example to List all Resource Tags + + tags, err = attributestags.List(context.TODO(), client, "networks", network.ID).Extract() + +Example to Delete all Resource Tags + + err = attributestags.DeleteAll(context.TODO(), client, "networks", network.ID).ExtractErr() + +Example to Add a tag to a Resource + + err = attributestags.Add(context.TODO(), client, "networks", network.ID, "atag").ExtractErr() + +Example to Delete a tag from a Resource + + err = attributestags.Delete(context.TODO(), client, "networks", network.ID, "atag").ExtractErr() + +Example to confirm if a tag exists on a resource + + exists, _ := attributestags.Confirm(context.TODO(), client, "networks", network.ID, "atag").Extract() +*/ +package attributestags diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/requests.go new file mode 100644 index 000000000..f22a57385 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/requests.go @@ -0,0 +1,89 @@ +package attributestags + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" +) + +// ReplaceAllOptsBuilder allows extensions to add additional parameters to +// the ReplaceAll request. +type ReplaceAllOptsBuilder interface { + ToAttributeTagsReplaceAllMap() (map[string]any, error) +} + +// ReplaceAllOpts provides options used to create Tags on a Resource +type ReplaceAllOpts struct { + Tags []string `json:"tags" required:"true"` +} + +// ToAttributeTagsReplaceAllMap formats a ReplaceAllOpts into the body of the +// replace request +func (opts ReplaceAllOpts) ToAttributeTagsReplaceAllMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// ReplaceAll updates all tags on a resource, replacing any existing tags +func ReplaceAll(ctx context.Context, client *gophercloud.ServiceClient, resourceType string, resourceID string, opts ReplaceAllOptsBuilder) (r ReplaceAllResult) { + b, err := opts.ToAttributeTagsReplaceAllMap() + url := replaceURL(client, resourceType, resourceID) + if err != nil { + r.Err = err + return + } + resp, err := client.Put(ctx, url, &b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// List all tags on a resource +func List(ctx context.Context, client *gophercloud.ServiceClient, resourceType string, resourceID string) (r ListResult) { + url := listURL(client, resourceType, resourceID) + resp, err := client.Get(ctx, url, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// DeleteAll deletes all tags on a resource +func DeleteAll(ctx context.Context, client *gophercloud.ServiceClient, resourceType string, resourceID string) (r DeleteResult) { + url := deleteAllURL(client, resourceType, resourceID) + resp, err := client.Delete(ctx, url, &gophercloud.RequestOpts{ + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Add a tag on a resource +func Add(ctx context.Context, client *gophercloud.ServiceClient, resourceType string, resourceID string, tag string) (r AddResult) { + url := addURL(client, resourceType, resourceID, tag) + resp, err := client.Put(ctx, url, nil, nil, &gophercloud.RequestOpts{ + OkCodes: []int{201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete a tag on a resource +func Delete(ctx context.Context, client *gophercloud.ServiceClient, resourceType string, resourceID string, tag string) (r DeleteResult) { + url := deleteURL(client, resourceType, resourceID, tag) + resp, err := client.Delete(ctx, url, &gophercloud.RequestOpts{ + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Confirm if a tag exists on a resource +func Confirm(ctx context.Context, client *gophercloud.ServiceClient, resourceType string, resourceID string, tag string) (r ConfirmResult) { + url := confirmURL(client, resourceType, resourceID, tag) + resp, err := client.Get(ctx, url, nil, &gophercloud.RequestOpts{ + OkCodes: []int{204}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/results.go new file mode 100644 index 000000000..9114cc7c2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/results.go @@ -0,0 +1,57 @@ +package attributestags + +import ( + "net/http" + + "github.com/gophercloud/gophercloud/v2" +) + +type tagResult struct { + gophercloud.Result +} + +// Extract interprets tagResult to return the list of tags +func (r tagResult) Extract() ([]string, error) { + var s struct { + Tags []string `json:"tags"` + } + err := r.ExtractInto(&s) + return s.Tags, err +} + +// ReplaceAllResult represents the result of a replace operation. +// Call its Extract method to interpret it as a slice of strings. +type ReplaceAllResult struct { + tagResult +} + +type ListResult struct { + tagResult +} + +// DeleteResult is the result from a Delete/DeleteAll operation. +// Call its ExtractErr method to determine if the call succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AddResult is the result from an Add operation. +// Call its ExtractErr method to determine if the call succeeded or failed. +type AddResult struct { + gophercloud.ErrResult +} + +// ConfirmResult is the result from an Confirm operation. +type ConfirmResult struct { + gophercloud.Result +} + +func (r ConfirmResult) Extract() (bool, error) { + exists := r.Err == nil + + if gophercloud.ResponseCodeIs(r.Err, http.StatusNotFound) { + r.Err = nil + } + + return exists, r.Err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/urls.go new file mode 100644 index 000000000..94eb2b41d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags/urls.go @@ -0,0 +1,31 @@ +package attributestags + +import "github.com/gophercloud/gophercloud/v2" + +const ( + tagsPath = "tags" +) + +func replaceURL(c *gophercloud.ServiceClient, r_type string, id string) string { + return c.ServiceURL(r_type, id, tagsPath) +} + +func listURL(c *gophercloud.ServiceClient, r_type string, id string) string { + return c.ServiceURL(r_type, id, tagsPath) +} + +func deleteAllURL(c *gophercloud.ServiceClient, r_type string, id string) string { + return c.ServiceURL(r_type, id, tagsPath) +} + +func addURL(c *gophercloud.ServiceClient, r_type string, id string, tag string) string { + return c.ServiceURL(r_type, id, tagsPath, tag) +} + +func deleteURL(c *gophercloud.ServiceClient, r_type string, id string, tag string) string { + return c.ServiceURL(r_type, id, tagsPath, tag) +} + +func confirmURL(c *gophercloud.ServiceClient, r_type string, id string, tag string) string { + return c.ServiceURL(r_type, id, tagsPath, tag) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/delegate.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/delegate.go new file mode 100644 index 000000000..870403a35 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/delegate.go @@ -0,0 +1,43 @@ +package extensions + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + common "github.com/gophercloud/gophercloud/v2/openstack/common/extensions" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// Extension is a single OpenStack extension. +type Extension struct { + common.Extension +} + +// GetResult wraps a GetResult from common. +type GetResult struct { + common.GetResult +} + +// ExtractExtensions interprets a Page as a slice of Extensions. +func ExtractExtensions(page pagination.Page) ([]Extension, error) { + inner, err := common.ExtractExtensions(page) + if err != nil { + return nil, err + } + outer := make([]Extension, len(inner)) + for index, ext := range inner { + outer[index] = Extension{ext} + } + return outer, nil +} + +// Get retrieves information for a specific extension using its alias. +func Get(ctx context.Context, c *gophercloud.ServiceClient, alias string) GetResult { + return GetResult{common.Get(ctx, c, alias)} +} + +// List returns a Pager which allows you to iterate over the full collection of extensions. +// It does not accept query parameters. +func List(c *gophercloud.ServiceClient) pagination.Pager { + return common.List(c) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/constants.go new file mode 100644 index 000000000..85dff7818 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/constants.go @@ -0,0 +1,7 @@ +package floatingips + +const ( + StatusActive = "ACTIVE" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/doc.go new file mode 100644 index 000000000..ab54042a4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/doc.go @@ -0,0 +1,71 @@ +/* +package floatingips enables management and retrieval of Floating IPs from the +OpenStack Networking service. + +Example to List Floating IPs + + listOpts := floatingips.ListOpts{ + FloatingNetworkID: "a6917946-38ab-4ffd-a55a-26c0980ce5ee", + } + + allPages, err := floatingips.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allFIPs, err := floatingips.ExtractFloatingIPs(allPages) + if err != nil { + panic(err) + } + + for _, fip := range allFIPs { + fmt.Printf("%+v\n", fip) + } + +Example to Create a Floating IP + + createOpts := floatingips.CreateOpts{ + FloatingNetworkID: "a6917946-38ab-4ffd-a55a-26c0980ce5ee", + } + + fip, err := floatingips.Create(context.TODO(), networkingClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Floating IP + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + portID := "76d0a61b-b8e5-490c-9892-4cf674f2bec8" + + updateOpts := floatingips.UpdateOpts{ + PortID: &portID, + } + + fip, err := floatingips.Update(context.TODO(), networkingClient, fipID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Disassociate a Floating IP with a Port + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + + updateOpts := floatingips.UpdateOpts{ + PortID: new(string), + } + + fip, err := floatingips.Update(context.TODO(), networkingClient, fipID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Floating IP + + fipID := "2f245a7b-796b-4f26-9cf9-9e82d248fda7" + err := floatingips.Delete(context.TODO(), networkClient, fipID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package floatingips diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/requests.go new file mode 100644 index 000000000..be8949d69 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/requests.go @@ -0,0 +1,206 @@ +package floatingips + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToFloatingIPListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Description string `q:"description"` + FloatingNetworkID string `q:"floating_network_id"` + PortID string `q:"port_id"` + FixedIP string `q:"fixed_ip_address"` + FloatingIP string `q:"floating_ip_address"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + RouterID string `q:"router_id"` + Status string `q:"status"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToFloatingIPListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// floating IP resources. It accepts a ListOpts struct, which allows you to +// filter and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToFloatingIPListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return FloatingIPPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToFloatingIPCreateMap() (map[string]any, error) +} + +// CreateOpts contains all the values needed to create a new floating IP +// resource. The only required fields are FloatingNetworkID and PortID which +// refer to the external network and internal port respectively. +type CreateOpts struct { + Description string `json:"description,omitempty"` + FloatingNetworkID string `json:"floating_network_id" required:"true"` + FloatingIP string `json:"floating_ip_address,omitempty"` + PortID string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` + SubnetID string `json:"subnet_id,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` +} + +// ToFloatingIPCreateMap allows CreateOpts to satisfy the CreateOptsBuilder +// interface +func (opts CreateOpts) ToFloatingIPCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "floatingip") +} + +// Create accepts a CreateOpts struct and uses the values provided to create a +// new floating IP resource. You can create floating IPs on external networks +// only. If you provide a FloatingNetworkID which refers to a network that is +// not external (i.e. its `router:external' attribute is False), the operation +// will fail and return a 400 error. +// +// If you do not specify a FloatingIP address value, the operation will +// automatically allocate an available address for the new resource. If you do +// choose to specify one, it must fall within the subnet range for the external +// network - otherwise the operation returns a 400 error. If the FloatingIP +// address is already in use, the operation returns a 409 error code. +// +// You can associate the new resource with an internal port by using the PortID +// field. If you specify a PortID that is not valid, the operation will fail and +// return 404 error code. +// +// You must also configure an IP address for the port associated with the PortID +// you have provided - this is what the FixedIP refers to: an IP fixed to a +// port. Because a port might be associated with multiple IP addresses, you can +// use the FixedIP field to associate a particular IP address rather than have +// the API assume for you. If you specify an IP address that is not valid, the +// operation will fail and return a 400 error code. If the PortID and FixedIP +// are already associated with another resource, the operation will fail and +// returns a 409 error code. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToFloatingIPCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular floating IP resource based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToFloatingIPUpdateMap() (map[string]any, error) +} + +// UpdateOpts contains the values used when updating a floating IP resource. The +// only value that can be updated is which internal port the floating IP is +// linked to. To associate the floating IP with a new internal port, provide its +// ID. To disassociate the floating IP from all ports, provide an empty string. +type UpdateOpts struct { + Description *string `json:"description,omitempty"` + PortID *string `json:"port_id,omitempty"` + FixedIP string `json:"fixed_ip_address,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToFloatingIPUpdateMap allows UpdateOpts to satisfy the UpdateOptsBuilder +// interface +func (opts UpdateOpts) ToFloatingIPUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "floatingip") + if err != nil { + return nil, err + } + + if m := b["floatingip"].(map[string]any); m["port_id"] == "" { + m["port_id"] = nil + } + + return b, nil +} + +// Update allows floating IP resources to be updated. Currently, the only way to +// "update" a floating IP is to associate it with a new internal port, or +// disassociated it from all ports. See UpdateOpts for instructions of how to +// do this. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToFloatingIPUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular floating IP resource. Please +// ensure this is what you want - you can also disassociate the IP from existing +// internal ports. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/results.go new file mode 100644 index 000000000..7ea616003 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/results.go @@ -0,0 +1,184 @@ +package floatingips + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// FloatingIP represents a floating IP resource. A floating IP is an external +// IP address that is mapped to an internal port and, optionally, a specific +// IP address on a private network. In other words, it enables access to an +// instance on a private network from an external network. For this reason, +// floating IPs can only be defined on networks where the `router:external' +// attribute (provided by the external network extension) is set to True. +type FloatingIP struct { + // ID is the unique identifier for the floating IP instance. + ID string `json:"id"` + + // Description for the floating IP instance. + Description string `json:"description"` + + // FloatingNetworkID is the UUID of the external network where the floating + // IP is to be created. + FloatingNetworkID string `json:"floating_network_id"` + + // FloatingIP is the address of the floating IP on the external network. + FloatingIP string `json:"floating_ip_address"` + + // PortID is the UUID of the port on an internal network that is associated + // with the floating IP. + PortID string `json:"port_id"` + + // FixedIP is the specific IP address of the internal port which should be + // associated with the floating IP. + FixedIP string `json:"fixed_ip_address"` + + // TenantID is the project owner of the floating IP. Only admin users can + // specify a project identifier other than its own. + TenantID string `json:"tenant_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of + // the floating ip last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // ProjectID is the project owner of the floating IP. + ProjectID string `json:"project_id"` + + // Status is the condition of the API resource. + Status string `json:"status"` + + // RouterID is the ID of the router used for this floating IP. + RouterID string `json:"router_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` +} + +func (r *FloatingIP) UnmarshalJSON(b []byte) error { + type tmp FloatingIP + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = FloatingIP(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = FloatingIP(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +type commonResult struct { + gophercloud.Result +} + +// Extract will extract a FloatingIP resource from a result. +func (r commonResult) Extract() (*FloatingIP, error) { + var s FloatingIP + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v any) error { + return r.Result.ExtractIntoStructPtr(v, "floatingip") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a FloatingIP. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a FloatingIP. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a FloatingIP. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of an update operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// FloatingIPPage is the page returned by a pager when traversing over a +// collection of floating IPs. +type FloatingIPPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of floating IPs has +// reached the end of a page and the pager seeks to traverse over a new one. +// In order to do this, it needs to construct the next page's URL. +func (r FloatingIPPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"floatingips_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a FloatingIPPage struct is empty. +func (r FloatingIPPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractFloatingIPs(r) + return len(is) == 0, err +} + +// ExtractFloatingIPs accepts a Page struct, specifically a FloatingIPPage +// struct, and extracts the elements into a slice of FloatingIP structs. In +// other words, a generic collection is mapped into a relevant slice. +func ExtractFloatingIPs(r pagination.Page) ([]FloatingIP, error) { + var s struct { + FloatingIPs []FloatingIP `json:"floatingips"` + } + err := (r.(FloatingIPPage)).ExtractInto(&s) + return s.FloatingIPs, err +} + +func ExtractFloatingIPsInto(r pagination.Page, v any) error { + return r.(FloatingIPPage).Result.ExtractIntoSlicePtr(v, "floatingips") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/urls.go new file mode 100644 index 000000000..4352321a3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips/urls.go @@ -0,0 +1,13 @@ +package floatingips + +import "github.com/gophercloud/gophercloud/v2" + +const resourcePath = "floatingips" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/doc.go new file mode 100644 index 000000000..42ccbcf7c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/doc.go @@ -0,0 +1,139 @@ +/* +Package routers enables management and retrieval of Routers from the OpenStack +Networking service. + +Example to List Routers + + listOpts := routers.ListOpts{} + allPages, err := routers.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allRouters, err := routers.ExtractRouters(allPages) + if err != nil { + panic(err) + } + + for _, router := range allRoutes { + fmt.Printf("%+v\n", router) + } + +Example to Create a Router + + iTrue := true + gwi := routers.GatewayInfo{ + NetworkID: "8ca37218-28ff-41cb-9b10-039601ea7e6b", + } + + createOpts := routers.CreateOpts{ + Name: "router_1", + AdminStateUp: &iTrue, + GatewayInfo: &gwi, + } + + router, err := routers.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Router + + routerID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + routes := []routers.Route{{ + DestinationCIDR: "40.0.1.0/24", + NextHop: "10.1.0.10", + }} + + updateOpts := routers.UpdateOpts{ + Name: "new_name", + Routes: &routes, + } + + router, err := routers.Update(context.TODO(), networkClient, routerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update just the Router name, keeping everything else as-is + + routerID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + updateOpts := routers.UpdateOpts{ + Name: "new_name", + } + + router, err := routers.Update(context.TODO(), networkClient, routerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove all Routes from a Router + + routerID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + routes := []routers.Route{} + + updateOpts := routers.UpdateOpts{ + Routes: &routes, + } + + router, err := routers.Update(context.TODO(), networkClient, routerID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Router + + routerID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + err := routers.Delete(context.TODO(), networkClient, routerID).ExtractErr() + if err != nil { + panic(err) + } + +Example to Add an Interface to a Router + + routerID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + intOpts := routers.AddInterfaceOpts{ + SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1", + } + + interface, err := routers.AddInterface(context.TODO(), networkClient, routerID, intOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove an Interface from a Router + + routerID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + intOpts := routers.RemoveInterfaceOpts{ + SubnetID: "a2f1f29d-571b-4533-907f-5803ab96ead1", + } + + interface, err := routers.RemoveInterface(context.TODO(), networkClient, routerID, intOpts).Extract() + if err != nil { + panic(err) + } + +Example to List an L3 agents for a Router + + routerID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + allPages, err := routers.ListL3Agents(networkClient, routerID).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allL3Agents, err := routers.ExtractL3Agents(allPages) + if err != nil { + panic(err) + } + + for _, agent := range allL3Agents { + fmt.Printf("%+v\n", agent) + } +*/ +package routers diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/requests.go new file mode 100644 index 000000000..def4699db --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/requests.go @@ -0,0 +1,284 @@ +package routers + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToRouterListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the floating IP attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + Distributed *bool `q:"distributed"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` +} + +// ToRouterListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToRouterListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// routers. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those routers that are owned by the +// tenant who submits the request, unless an admin user submits the request. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToRouterListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return RouterPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToRouterCreateMap() (map[string]any, error) +} + +// CreateOpts contains all the values needed to create a new router. There are +// no required values. +type CreateOpts struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Distributed *bool `json:"distributed,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` + AvailabilityZoneHints []string `json:"availability_zone_hints,omitempty"` +} + +// ToRouterCreateMap builds a create request body from CreateOpts. +func (opts CreateOpts) ToRouterCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "router") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical router. When it is created, the router does not have an internal +// interface - it is not associated to any subnet. +// +// You can optionally specify an external gateway for a router using the +// GatewayInfo struct. The external gateway for the router must be plugged into +// an external network (it is external if its `router:external' field is set to +// true). +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToRouterCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular router based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToRouterUpdateMap() (map[string]any, error) +} + +// UpdateOpts contains the values used when updating a router. +type UpdateOpts struct { + Name string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Distributed *bool `json:"distributed,omitempty"` + GatewayInfo *GatewayInfo `json:"external_gateway_info,omitempty"` + Routes *[]Route `json:"routes,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToRouterUpdateMap builds an update body based on UpdateOpts. +func (opts UpdateOpts) ToRouterUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "router") +} + +// Update allows routers to be updated. You can update the name, administrative +// state, and the external gateway. For more information about how to set the +// external gateway for a router, see Create. This operation does not enable +// the update of router interfaces. To do this, use the AddInterface and +// RemoveInterface functions. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToRouterUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular router based on its unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// AddInterfaceOptsBuilder allows extensions to add additional parameters to +// the AddInterface request. +type AddInterfaceOptsBuilder interface { + ToRouterAddInterfaceMap() (map[string]any, error) +} + +// AddInterfaceOpts represents the options for adding an interface to a router. +type AddInterfaceOpts struct { + SubnetID string `json:"subnet_id,omitempty" xor:"PortID"` + PortID string `json:"port_id,omitempty" xor:"SubnetID"` +} + +// ToRouterAddInterfaceMap builds a request body from AddInterfaceOpts. +func (opts AddInterfaceOpts) ToRouterAddInterfaceMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// AddInterface attaches a subnet to an internal router interface. You must +// specify either a SubnetID or PortID in the request body. If you specify both, +// the operation will fail and an error will be returned. +// +// If you specify a SubnetID, the gateway IP address for that particular subnet +// is used to create the router interface. Alternatively, if you specify a +// PortID, the IP address associated with the port is used to create the router +// interface. +// +// If you reference a port that is associated with multiple IP addresses, or +// if the port is associated with zero IP addresses, the operation will fail and +// a 400 Bad Request error will be returned. +// +// If you reference a port already in use, the operation will fail and a 409 +// Conflict error will be returned. +// +// The PortID that is returned after using Extract() on the result of this +// operation can either be the same PortID passed in or, on the other hand, the +// identifier of a new port created by this operation. After the operation +// completes, the device ID of the port is set to the router ID, and the +// device owner attribute is set to `network:router_interface'. +func AddInterface(ctx context.Context, c *gophercloud.ServiceClient, id string, opts AddInterfaceOptsBuilder) (r InterfaceResult) { + b, err := opts.ToRouterAddInterfaceMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, addInterfaceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// RemoveInterfaceOptsBuilder allows extensions to add additional parameters to +// the RemoveInterface request. +type RemoveInterfaceOptsBuilder interface { + ToRouterRemoveInterfaceMap() (map[string]any, error) +} + +// RemoveInterfaceOpts represents options for removing an interface from +// a router. +type RemoveInterfaceOpts struct { + SubnetID string `json:"subnet_id,omitempty" or:"PortID"` + PortID string `json:"port_id,omitempty" or:"SubnetID"` +} + +// ToRouterRemoveInterfaceMap builds a request body based on +// RemoveInterfaceOpts. +func (opts RemoveInterfaceOpts) ToRouterRemoveInterfaceMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +// RemoveInterface removes an internal router interface, which detaches a +// subnet from the router. You must specify either a SubnetID or PortID, since +// these values are used to identify the router interface to remove. +// +// Unlike AddInterface, you can also specify both a SubnetID and PortID. If you +// choose to specify both, the subnet ID must correspond to the subnet ID of +// the first IP address on the port specified by the port ID. Otherwise, the +// operation will fail and return a 409 Conflict error. +// +// If the router, subnet or port which are referenced do not exist or are not +// visible to you, the operation will fail and a 404 Not Found error will be +// returned. After this operation completes, the port connecting the router +// with the subnet is removed from the subnet for the network. +func RemoveInterface(ctx context.Context, c *gophercloud.ServiceClient, id string, opts RemoveInterfaceOptsBuilder) (r InterfaceResult) { + b, err := opts.ToRouterRemoveInterfaceMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, removeInterfaceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListL3Agents returns a list of l3-agents scheduled for a specific router. +func ListL3Agents(c *gophercloud.ServiceClient, id string) (result pagination.Pager) { + return pagination.NewPager(c, listl3AgentsURL(c, id), func(r pagination.PageResult) pagination.Page { + return ListL3AgentsPage{pagination.SinglePageBase(r)} + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/results.go new file mode 100644 index 000000000..d657160ba --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/results.go @@ -0,0 +1,336 @@ +package routers + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// GatewayInfo represents the information of an external gateway for any +// particular network router. +type GatewayInfo struct { + NetworkID string `json:"network_id,omitempty"` + EnableSNAT *bool `json:"enable_snat,omitempty"` + ExternalFixedIPs []ExternalFixedIP `json:"external_fixed_ips,omitempty"` + QoSPolicyID string `json:"qos_policy_id,omitempty"` +} + +// ExternalFixedIP is the IP address and subnet ID of the external gateway of a +// router. +type ExternalFixedIP struct { + IPAddress string `json:"ip_address,omitempty"` + SubnetID string `json:"subnet_id,omitempty"` +} + +// Route is a possible route in a router. +type Route struct { + NextHop string `json:"nexthop"` + DestinationCIDR string `json:"destination"` +} + +// Router represents a Neutron router. A router is a logical entity that +// forwards packets across internal subnets and NATs (network address +// translation) them on external networks through an appropriate gateway. +// +// A router has an interface for each subnet with which it is associated. By +// default, the IP address of such interface is the subnet's gateway IP. Also, +// whenever a router is associated with a subnet, a port for that router +// interface is added to the subnet's network. +type Router struct { + // Status indicates whether or not a router is currently operational. + Status string `json:"status"` + + // GateayInfo provides information on external gateway for the router. + GatewayInfo GatewayInfo `json:"external_gateway_info"` + + // AdminStateUp is the administrative state of the router. + AdminStateUp bool `json:"admin_state_up"` + + // Distributed is whether router is disitrubted or not. + Distributed bool `json:"distributed"` + + // Name is the human readable name for the router. It does not have to be + // unique. + Name string `json:"name"` + + // Description for the router. + Description string `json:"description"` + + // ID is the unique identifier for the router. + ID string `json:"id"` + + // TenantID is the project owner of the router. Only admin users can + // specify a project identifier other than its own. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the router. + ProjectID string `json:"project_id"` + + // Routes are a collection of static routes that the router will host. + Routes []Route `json:"routes"` + + // Availability zone hints groups network nodes that run services like DHCP, L3, FW, and others. + // Used to make network resources highly available. + AvailabilityZoneHints []string `json:"availability_zone_hints"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // Timestamp when the router was created + CreatedAt time.Time `json:"-"` + + // Timestamp when the router was last updated + UpdatedAt time.Time `json:"-"` +} + +func (r *Router) UnmarshalJSON(b []byte) error { + type tmp Router + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Router(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Router(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// RouterPage is the page returned by a pager when traversing over a +// collection of routers. +type RouterPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of routers has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r RouterPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"routers_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a RouterPage struct is empty. +func (r RouterPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractRouters(r) + return len(is) == 0, err +} + +// ExtractRouters accepts a Page struct, specifically a RouterPage struct, +// and extracts the elements into a slice of Router structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRouters(r pagination.Page) ([]Router, error) { + var s []Router + err := ExtractRoutersInto(r, &s) + return s, err +} + +// ExtractRoutersInto extracts the elements into a slice of Router structs. +func ExtractRoutersInto(r pagination.Page, v any) error { + return r.(RouterPage).Result.ExtractIntoSlicePtr(v, "routers") +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a router. +func (r commonResult) Extract() (*Router, error) { + var s struct { + Router *Router `json:"router"` + } + err := r.ExtractInto(&s) + return s.Router, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Router. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Router. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Router. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its ExtractErr +// method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// InterfaceInfo represents information about a particular router interface. As +// mentioned above, in order for a router to forward to a subnet, it needs an +// interface. +type InterfaceInfo struct { + // SubnetID is the ID of the subnet which this interface is associated with. + SubnetID string `json:"subnet_id"` + + // PortID is the ID of the port that is a part of the subnet. + PortID string `json:"port_id"` + + // ID is the UUID of the interface. + ID string `json:"id"` + + // TenantID is the owner of the interface. + TenantID string `json:"tenant_id"` +} + +// InterfaceResult represents the result of interface operations, such as +// AddInterface() and RemoveInterface(). Call its Extract method to interpret +// the result as a InterfaceInfo. +type InterfaceResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts an information struct. +func (r InterfaceResult) Extract() (*InterfaceInfo, error) { + var s InterfaceInfo + err := r.ExtractInto(&s) + return &s, err +} + +// L3Agent represents a Neutron agent for routers. +type L3Agent struct { + // ID is the id of the agent. + ID string `json:"id"` + + // AdminStateUp is an administrative state of the agent. + AdminStateUp bool `json:"admin_state_up"` + + // AgentType is a type of the agent. + AgentType string `json:"agent_type"` + + // Alive indicates whether agent is alive or not. + Alive bool `json:"alive"` + + // ResourcesSynced indicates whether agent is synced or not. + // Not all agent types track resources via Placement. + ResourcesSynced bool `json:"resources_synced"` + + // AvailabilityZone is a zone of the agent. + AvailabilityZone string `json:"availability_zone"` + + // Binary is an executable binary of the agent. + Binary string `json:"binary"` + + // Configurations is a configuration specific key/value pairs that are + // determined by the agent binary and type. + Configurations map[string]any `json:"configurations"` + + // CreatedAt is a creation timestamp. + CreatedAt time.Time `json:"-"` + + // StartedAt is a starting timestamp. + StartedAt time.Time `json:"-"` + + // HeartbeatTimestamp is a last heartbeat timestamp. + HeartbeatTimestamp time.Time `json:"-"` + + // Description contains agent description. + Description string `json:"description"` + + // Host is a hostname of the agent system. + Host string `json:"host"` + + // Topic contains name of AMQP topic. + Topic string `json:"topic"` + + // HAState is a ha state of agent(active/standby) for router + HAState string `json:"ha_state"` + + // ResourceVersions is a list agent known objects and version numbers + ResourceVersions map[string]any `json:"resource_versions"` +} + +// UnmarshalJSON helps to convert the timestamps into the time.Time type. +func (r *L3Agent) UnmarshalJSON(b []byte) error { + type tmp L3Agent + var s struct { + tmp + CreatedAt gophercloud.JSONRFC3339ZNoTNoZ `json:"created_at"` + StartedAt gophercloud.JSONRFC3339ZNoTNoZ `json:"started_at"` + HeartbeatTimestamp gophercloud.JSONRFC3339ZNoTNoZ `json:"heartbeat_timestamp"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = L3Agent(s.tmp) + + r.CreatedAt = time.Time(s.CreatedAt) + r.StartedAt = time.Time(s.StartedAt) + r.HeartbeatTimestamp = time.Time(s.HeartbeatTimestamp) + + return nil +} + +type ListL3AgentsPage struct { + pagination.SinglePageBase +} + +func (r ListL3AgentsPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + v, err := ExtractL3Agents(r) + return len(v) == 0, err +} + +func ExtractL3Agents(r pagination.Page) ([]L3Agent, error) { + var s struct { + L3Agents []L3Agent `json:"agents"` + } + + err := (r.(ListL3AgentsPage)).ExtractInto(&s) + return s.L3Agents, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/urls.go new file mode 100644 index 000000000..87815aa6e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers/urls.go @@ -0,0 +1,25 @@ +package routers + +import "github.com/gophercloud/gophercloud/v2" + +const resourcePath = "routers" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func addInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "add_router_interface") +} + +func removeInterfaceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "remove_router_interface") +} + +func listl3AgentsURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "l3-agents") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/doc.go new file mode 100644 index 000000000..298d61712 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/doc.go @@ -0,0 +1,58 @@ +/* +Package groups provides information and interaction with Security Groups +for the OpenStack Networking service. + +Example to List Security Groups + + listOpts := groups.ListOpts{ + TenantID: "966b3c7d36a24facaf20b7e458bf2192", + } + + allPages, err := groups.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allGroups, err := groups.ExtractGroups(allPages) + if err != nil { + panic(err) + } + + for _, group := range allGroups { + fmt.Printf("%+v\n", group) + } + +Example to Create a Security Group + + createOpts := groups.CreateOpts{ + Name: "group_name", + Description: "A Security Group", + } + + group, err := groups.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Security Group + + groupID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + + updateOpts := groups.UpdateOpts{ + Name: "new_name", + } + + group, err := groups.Update(context.TODO(), networkClient, groupID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group + + groupID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + err := groups.Delete(context.TODO(), networkClient, groupID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package groups diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/requests.go new file mode 100644 index 000000000..95f7bfabd --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/requests.go @@ -0,0 +1,160 @@ +package groups + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the group attributes you want to see returned. SortKey allows you to +// sort by a particular network attribute. SortDir sets the direction, and is +// either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + ID string `q:"id"` + Name string `q:"name"` + Description string `q:"description"` + Stateful *bool `q:"stateful"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` +} + +// List returns a Pager which allows you to iterate over a collection of +// security groups. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOpts) pagination.Pager { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return pagination.Pager{Err: err} + } + u := rootURL(c) + q.String() + return pagination.NewPager(c, u, func(r pagination.PageResult) pagination.Page { + return SecGroupPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecGroupCreateMap() (map[string]any, error) +} + +// CreateOpts contains all the values needed to create a new security group. +type CreateOpts struct { + // Human-readable name for the Security Group. Does not have to be unique. + Name string `json:"name" required:"true"` + + // TenantID is the UUID of the project who owns the Group. + // Only administrative users can specify a tenant UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // ProjectID is the UUID of the project who owns the Group. + // Only administrative users can specify a tenant UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // Describes the security group. + Description string `json:"description,omitempty"` + + // Stateful indicates if the security group is stateful or stateless. + Stateful *bool `json:"stateful,omitempty"` +} + +// ToSecGroupCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecGroupCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Create is an operation which provisions a new security group with default +// security group rules for the IPv4 and IPv6 ether types. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSecGroupUpdateMap() (map[string]any, error) +} + +// UpdateOpts contains all the values needed to update an existing security +// group. +type UpdateOpts struct { + // Human-readable name for the Security Group. Does not have to be unique. + Name string `json:"name,omitempty"` + + // Describes the security group. + Description *string `json:"description,omitempty"` + + // Stateful indicates if the security group is stateful or stateless. + Stateful *bool `json:"stateful,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToSecGroupUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSecGroupUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "security_group") +} + +// Update is an operation which updates an existing security group. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSecGroupUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + + resp, err := c.Put(ctx, resourceURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular security group based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular security group based on its +// unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/results.go new file mode 100644 index 000000000..6a8a16fa8 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/results.go @@ -0,0 +1,164 @@ +package groups + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// SecGroup represents a container for security group rules. +type SecGroup struct { + // The UUID for the security group. + ID string + + // Human-readable name for the security group. Might not be unique. + // Cannot be named "default" as that is automatically created for a tenant. + Name string + + // The security group description. + Description string + + // A slice of security group rules that dictate the permitted behaviour for + // traffic entering and leaving the group. + Rules []rules.SecGroupRule `json:"security_group_rules"` + + // Indicates if the security group is stateful or stateless. + Stateful bool `json:"stateful"` + + // TenantID is the project owner of the security group. + TenantID string `json:"tenant_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // security group last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // ProjectID is the project owner of the security group. + ProjectID string `json:"project_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` +} + +func (r *SecGroup) UnmarshalJSON(b []byte) error { + type tmp SecGroup + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = SecGroup(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = SecGroup(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// SecGroupPage is the page returned by a pager when traversing over a +// collection of security groups. +type SecGroupPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security groups has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (r SecGroupPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"security_groups_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SecGroupPage struct is empty. +func (r SecGroupPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractGroups(r) + return len(is) == 0, err +} + +// ExtractGroups accepts a Page struct, specifically a SecGroupPage struct, +// and extracts the elements into a slice of SecGroup structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractGroups(r pagination.Page) ([]SecGroup, error) { + var s struct { + SecGroups []SecGroup `json:"security_groups"` + } + err := (r.(SecGroupPage)).ExtractInto(&s) + return s.SecGroups, err +} + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security group. +func (r commonResult) Extract() (*SecGroup, error) { + var s struct { + SecGroup *SecGroup `json:"security_group"` + } + err := r.ExtractInto(&s) + return s.SecGroup, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a SecGroup. +type CreateResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a SecGroup. +type UpdateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a SecGroup. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/urls.go new file mode 100644 index 000000000..8c445868a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups/urls.go @@ -0,0 +1,13 @@ +package groups + +import "github.com/gophercloud/gophercloud/v2" + +const rootPath = "security-groups" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/doc.go new file mode 100644 index 000000000..fde5e1305 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/doc.go @@ -0,0 +1,50 @@ +/* +Package rules provides information and interaction with Security Group Rules +for the OpenStack Networking service. + +Example to List Security Groups Rules + + listOpts := rules.ListOpts{ + Protocol: "tcp", + } + + allPages, err := rules.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allRules, err := rules.ExtractRules(allPages) + if err != nil { + panic(err) + } + + for _, rule := range allRules { + fmt.Printf("%+v\n", rule) + } + +Example to Create a Security Group Rule + + createOpts := rules.CreateOpts{ + Direction: "ingress", + PortRangeMin: 80, + EtherType: rules.EtherType4, + PortRangeMax: 80, + Protocol: "tcp", + RemoteGroupID: "85cc3048-abc3-43cc-89b3-377341426ac5", + SecGroupID: "a7734e61-b545-452d-a3cd-0189cbd9747a", + } + + rule, err := rules.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Security Group Rule + + ruleID := "37d94f8a-d136-465c-ae46-144f0d8ef141" + err := rules.Delete(context.TODO(), networkClient, ruleID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package rules diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/requests.go new file mode 100644 index 000000000..edd253f03 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/requests.go @@ -0,0 +1,207 @@ +package rules + +import ( + "context" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the List +// request. +type ListOptsBuilder interface { + ToSecGroupListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the security group rule attributes you want to see returned. SortKey allows +// you to sort by a particular network attribute. SortDir sets the direction, +// and is either `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Direction string `q:"direction"` + EtherType string `q:"ethertype"` + ID string `q:"id"` + Description string `q:"description"` + PortRangeMax int `q:"port_range_max"` + PortRangeMin int `q:"port_range_min"` + Protocol string `q:"protocol"` + RemoteAddressGroupID string `q:"remote_address_group_id"` + RemoteGroupID string `q:"remote_group_id"` + RemoteIPPrefix string `q:"remote_ip_prefix"` + SecGroupID string `q:"security_group_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + RevisionNumber *int `q:"revision_number"` +} + +// ToSecGroupListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSecGroupListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(&opts) + if err != nil { + return "", err + } + return q.String(), nil +} + +// List returns a Pager which allows you to iterate over a collection of +// security group rules. It accepts a ListOpts struct, which allows you to filter +// and sort the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := rootURL(c) + if opts != nil { + query, err := opts.ToSecGroupListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SecGroupRulePage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +type RuleDirection string +type RuleProtocol string +type RuleEtherType string + +// Constants useful for CreateOpts +const ( + DirIngress RuleDirection = "ingress" + DirEgress RuleDirection = "egress" + EtherType4 RuleEtherType = "IPv4" + EtherType6 RuleEtherType = "IPv6" + ProtocolAH RuleProtocol = "ah" + ProtocolDCCP RuleProtocol = "dccp" + ProtocolEGP RuleProtocol = "egp" + ProtocolESP RuleProtocol = "esp" + ProtocolGRE RuleProtocol = "gre" + ProtocolICMP RuleProtocol = "icmp" + ProtocolIGMP RuleProtocol = "igmp" + ProtocolIPIP RuleProtocol = "ipip" + ProtocolIPv6Encap RuleProtocol = "ipv6-encap" + ProtocolIPv6Frag RuleProtocol = "ipv6-frag" + ProtocolIPv6ICMP RuleProtocol = "ipv6-icmp" + ProtocolIPv6NoNxt RuleProtocol = "ipv6-nonxt" + ProtocolIPv6Opts RuleProtocol = "ipv6-opts" + ProtocolIPv6Route RuleProtocol = "ipv6-route" + ProtocolOSPF RuleProtocol = "ospf" + ProtocolPGM RuleProtocol = "pgm" + ProtocolRSVP RuleProtocol = "rsvp" + ProtocolSCTP RuleProtocol = "sctp" + ProtocolTCP RuleProtocol = "tcp" + ProtocolUDP RuleProtocol = "udp" + ProtocolUDPLite RuleProtocol = "udplite" + ProtocolVRRP RuleProtocol = "vrrp" + ProtocolAny RuleProtocol = "" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToSecGroupRuleCreateMap() (map[string]any, error) +} + +// CreateOpts contains all the values needed to create a new security group +// rule. +type CreateOpts struct { + // Must be either "ingress" or "egress": the direction in which the security + // group rule is applied. + Direction RuleDirection `json:"direction" required:"true"` + + // String description of each rule, optional + Description string `json:"description,omitempty"` + + // Must be "IPv4" or "IPv6", and addresses represented in CIDR must match the + // ingress or egress rules. + EtherType RuleEtherType `json:"ethertype" required:"true"` + + // The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id" required:"true"` + + // The maximum port number in the range that is matched by the security group + // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If + // the protocol is ICMP, this value must be an ICMP code. + PortRangeMax int `json:"port_range_max,omitempty"` + + // The minimum port number in the range that is matched by the security group + // rule. If the protocol is TCP or UDP, this value must be less than or equal + // to the value of the PortRangeMax attribute. If the protocol is ICMP, this + // value must be an ICMP type. + PortRangeMin int `json:"port_range_min,omitempty"` + + // The protocol that is matched by the security group rule. Valid values are + // "tcp", "udp", "icmp" or an empty string. + Protocol RuleProtocol `json:"protocol,omitempty"` + + // The remote address group ID to be associated with this security group rule. + // You can specify either RemoteAddressGroupID, RemoteGroupID, or RemoteIPPrefix + RemoteAddressGroupID string `json:"remote_address_group_id,omitempty"` + + // The remote group ID to be associated with this security group rule. You can + // specify either RemoteAddressGroupID,RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id,omitempty"` + + // The remote IP prefix to be associated with this security group rule. You can + // specify either RemoteAddressGroupID,RemoteGroupID or RemoteIPPrefix. This attribute matches the + // specified IP prefix as the source IP address of the IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix,omitempty"` + + // TenantID is the UUID of the project who owns the Rule. + // Only administrative users can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` +} + +// ToSecGroupRuleCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSecGroupRuleCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "security_group_rule") +} + +// Create is an operation which adds a new security group rule and associates it +// with an existing security group (whose ID is specified in CreateOpts). +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSecGroupRuleCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, rootURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateBulk is an operation which adds new security group rules and associates them +// with an existing security group (whose ID is specified in CreateOpts). +// As of Dalmatian (2024.2) neutron only allows bulk creation of rules when +// they all belong to the same tenant and security group. +// https://github.com/openstack/neutron/blob/6183792/neutron/db/securitygroups_db.py#L814-L828 +func CreateBulk(ctx context.Context, c *gophercloud.ServiceClient, opts []CreateOpts) (r CreateBulkResult) { + body, err := gophercloud.BuildRequestBody(opts, "security_group_rules") + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(ctx, rootURL(c), body, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Get retrieves a particular security group rule based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, resourceURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete will permanently delete a particular security group rule based on its +// unique ID. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, resourceURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/results.go new file mode 100644 index 000000000..03696ac20 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/results.go @@ -0,0 +1,204 @@ +package rules + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// SecGroupRule represents a rule to dictate the behaviour of incoming or +// outgoing traffic for a particular security group. +type SecGroupRule struct { + // The UUID for this security group rule. + ID string + + // The direction in which the security group rule is applied. The only values + // allowed are "ingress" or "egress". For a compute instance, an ingress + // security group rule is applied to incoming (ingress) traffic for that + // instance. An egress rule is applied to traffic leaving the instance. + Direction string + + // Description of the rule + Description string `json:"description"` + + // Must be IPv4 or IPv6, and addresses represented in CIDR must match the + // ingress or egress rules. + EtherType string `json:"ethertype"` + + // The security group ID to associate with this security group rule. + SecGroupID string `json:"security_group_id"` + + // The minimum port number in the range that is matched by the security group + // rule. If the protocol is TCP or UDP, this value must be less than or equal + // to the value of the PortRangeMax attribute. If the protocol is ICMP, this + // value must be an ICMP type. + PortRangeMin int `json:"port_range_min"` + + // The maximum port number in the range that is matched by the security group + // rule. The PortRangeMin attribute constrains the PortRangeMax attribute. If + // the protocol is ICMP, this value must be an ICMP type. + PortRangeMax int `json:"port_range_max"` + + // The protocol that is matched by the security group rule. Valid values are + // "tcp", "udp", "icmp" or an empty string. + Protocol string + + // The remote address group ID to be associated with this security group rule. + // You can specify either RemoteAddressGroupID, RemoteGroupID, or RemoteIPPrefix + RemoteAddressGroupID string `json:"remote_address_group_id"` + + // The remote group ID to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix. + RemoteGroupID string `json:"remote_group_id"` + + // The remote IP prefix to be associated with this security group rule. You + // can specify either RemoteGroupID or RemoteIPPrefix . This attribute + // matches the specified IP prefix as the source IP address of the IP packet. + RemoteIPPrefix string `json:"remote_ip_prefix"` + + // TenantID is the project owner of this security group rule. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of this security group rule. + ProjectID string `json:"project_id"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // Timestamp when the rule was created + CreatedAt time.Time `json:"-"` + + // Timestamp when the rule was last updated + UpdatedAt time.Time `json:"-"` +} + +func (r *SecGroupRule) UnmarshalJSON(b []byte) error { + type tmp SecGroupRule + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = SecGroupRule(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = SecGroupRule(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// SecGroupRulePage is the page returned by a pager when traversing over a +// collection of security group rules. +type SecGroupRulePage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of security group rules has +// reached the end of a page and the pager seeks to traverse over a new one. In +// order to do this, it needs to construct the next page's URL. +func (r SecGroupRulePage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"security_group_rules_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SecGroupRulePage struct is empty. +func (r SecGroupRulePage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractRules(r) + return len(is) == 0, err +} + +// ExtractRules accepts a Page struct, specifically a SecGroupRulePage struct, +// and extracts the elements into a slice of SecGroupRule structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractRules(r pagination.Page) ([]SecGroupRule, error) { + var s struct { + SecGroupRules []SecGroupRule `json:"security_group_rules"` + } + err := (r.(SecGroupRulePage)).ExtractInto(&s) + return s.SecGroupRules, err +} + +type commonResult struct { + gophercloud.Result +} + +type bulkResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a security rule. +func (r commonResult) Extract() (*SecGroupRule, error) { + var s struct { + SecGroupRule *SecGroupRule `json:"security_group_rule"` + } + err := r.ExtractInto(&s) + return s.SecGroupRule, err +} + +// Extract is a function that accepts a result and extracts security rules. +func (r bulkResult) Extract() ([]SecGroupRule, error) { + var s struct { + SecGroupRules []SecGroupRule `json:"security_group_rules"` + } + err := r.ExtractInto(&s) + return s.SecGroupRules, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a SecGroupRule. +type CreateResult struct { + commonResult +} + +// CreateBulkResult represents the result of a bulk create operation. Call its +// Extract method to interpret it as a slice of SecGroupRules. +type CreateBulkResult struct { + bulkResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a SecGroupRule. +type GetResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/urls.go new file mode 100644 index 000000000..98c9ea7c2 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules/urls.go @@ -0,0 +1,13 @@ +package rules + +import "github.com/gophercloud/gophercloud/v2" + +const rootPath = "security-group-rules" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(rootPath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/constants.go new file mode 100644 index 000000000..6bec77fa7 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/constants.go @@ -0,0 +1,9 @@ +package trunks + +const ( + StatusActive = "ACTIVE" + StatusBuild = "BUILD" + StatusDegraded = "DEGRADED" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/doc.go new file mode 100644 index 000000000..5da9d90e0 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/doc.go @@ -0,0 +1,143 @@ +/* +Package trunks provides the ability to retrieve and manage trunks through the Neutron API. +Trunks allow you to multiplex multiple ports traffic on a single port. For example, you could +have a compute instance port be the parent port of a trunk and inside the VM run workloads +using other ports, without the need of plugging those ports. + +Example of a new empty Trunk creation + + iTrue := true + createOpts := trunks.CreateOpts{ + Name: "gophertrunk", + Description: "Trunk created by gophercloud", + AdminStateUp: &iTrue, + PortID: "a6f0560c-b7a8-401f-bf6e-d0a5c851ae10", + } + + trunk, err := trunks.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + fmt.Printf("%+v\n", trunk) + +Example of a new Trunk creation with 2 subports + + iTrue := true + createOpts := trunks.CreateOpts{ + Name: "gophertrunk", + Description: "Trunk created by gophercloud", + AdminStateUp: &iTrue, + PortID: "a6f0560c-b7a8-401f-bf6e-d0a5c851ae10", + Subports: []trunks.Subport{ + { + SegmentationID: 1, + SegmentationType: "vlan", + PortID: "bf4efcc0-b1c7-4674-81f0-31f58a33420a", + }, + { + SegmentationID: 10, + SegmentationType: "vlan", + PortID: "2cf671b9-02b3-4121-9e85-e0af3548d112", + }, + }, + } + + trunk, err := trunks.Create(context.TODO(), client, createOpts).Extract() + if err != nil { + panic(err) + } + fmt.Printf("%+v\n", trunk) + +Example of deleting a Trunk + + trunkID := "c36e7f2e-0c53-4742-8696-aee77c9df159" + err := trunks.Delete(context.TODO(), networkClient, trunkID).ExtractErr() + if err != nil { + panic(err) + } + +Example of listing Trunks + + listOpts := trunks.ListOpts{} + allPages, err := trunks.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + allTrunks, err := trunks.ExtractTrunks(allPages) + if err != nil { + panic(err) + } + for _, trunk := range allTrunks { + fmt.Printf("%+v\n", trunk) + } + +Example of getting a Trunk + + trunkID = "52d8d124-3dc9-4563-9fef-bad3187ecf2d" + trunk, err := trunks.Get(context.TODO(), networkClient, trunkID).Extract() + if err != nil { + panic(err) + } + fmt.Printf("%+v\n", trunk) + +Example of updating a Trunk + + trunkID := "c36e7f2e-0c53-4742-8696-aee77c9df159" + subports, err := trunks.GetSubports(context.TODO(), client, trunkID).Extract() + iFalse := false + updateOpts := trunks.UpdateOpts{ + AdminStateUp: &iFalse, + Name: "updated_gophertrunk", + Description: "trunk updated by gophercloud", + } + trunk, err = trunks.Update(context.TODO(), networkClient, trunkID, updateOpts).Extract() + if err != nil { + log.Fatal(err) + } + fmt.Printf("%+v\n", trunk) + +Example of showing subports of a Trunk + + trunkID := "c36e7f2e-0c53-4742-8696-aee77c9df159" + subports, err := trunks.GetSubports(context.TODO(), client, trunkID).Extract() + fmt.Printf("%+v\n", subports) + +Example of adding two subports to a Trunk + + trunkID := "c36e7f2e-0c53-4742-8696-aee77c9df159" + addSubportsOpts := trunks.AddSubportsOpts{ + Subports: []trunks.Subport{ + { + SegmentationID: 1, + SegmentationType: "vlan", + PortID: "bf4efcc0-b1c7-4674-81f0-31f58a33420a", + }, + { + SegmentationID: 10, + SegmentationType: "vlan", + PortID: "2cf671b9-02b3-4121-9e85-e0af3548d112", + }, + }, + } + trunk, err := trunks.AddSubports(context.TODO(), client, trunkID, addSubportsOpts).Extract() + if err != nil { + panic(err) + } + fmt.Printf("%+v\n", trunk) + +Example of deleting two subports from a Trunk + + trunkID := "c36e7f2e-0c53-4742-8696-aee77c9df159" + removeSubportsOpts := trunks.RemoveSubportsOpts{ + Subports: []trunks.RemoveSubport{ + {PortID: "bf4efcc0-b1c7-4674-81f0-31f58a33420a"}, + {PortID: "2cf671b9-02b3-4121-9e85-e0af3548d112"}, + }, + } + trunk, err := trunks.RemoveSubports(context.TODO(), networkClient, trunkID, removeSubportsOpts).Extract() + if err != nil { + panic(err) + } + fmt.Printf("%+v\n", trunk) +*/ +package trunks diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/requests.go new file mode 100644 index 000000000..ea4dba250 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/requests.go @@ -0,0 +1,222 @@ +package trunks + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToTrunkCreateMap() (map[string]any, error) +} + +// CreateOpts represents the attributes used when creating a new trunk. +type CreateOpts struct { + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + PortID string `json:"port_id" required:"true"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Subports []Subport `json:"sub_ports"` +} + +// ToTrunkCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToTrunkCreateMap() (map[string]any, error) { + if opts.Subports == nil { + opts.Subports = []Subport{} + } + return gophercloud.BuildRequestBody(opts, "trunk") +} + +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + body, err := opts.ToTrunkCreateMap() + if err != nil { + r.Err = err + return + } + + resp, err := c.Post(ctx, createURL(c), body, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the trunk associated with it. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, deleteURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToTrunkListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the trunk attributes you want to see returned. SortKey allows you to sort +// by a particular trunk attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + AdminStateUp *bool `q:"admin_state_up"` + Description string `q:"description"` + ID string `q:"id"` + Name string `q:"name"` + PortID string `q:"port_id"` + Status string `q:"status"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + SortDir string `q:"sort_dir"` + SortKey string `q:"sort_key"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + // TODO change type to *int for consistency + RevisionNumber string `q:"revision_number"` +} + +// ToTrunkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToTrunkListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// trunks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those trunks that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToTrunkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return TrunkPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific trunk based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +type UpdateOptsBuilder interface { + ToTrunkUpdateMap() (map[string]any, error) +} + +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +func (opts UpdateOpts) ToTrunkUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "trunk") +} + +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + body, err := opts.ToTrunkUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(ctx, updateURL(c, id), body, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +func GetSubports(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetSubportsResult) { + resp, err := c.Get(ctx, getSubportsURL(c, id), &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +type AddSubportsOpts struct { + Subports []Subport `json:"sub_ports" required:"true"` +} + +type AddSubportsOptsBuilder interface { + ToTrunkAddSubportsMap() (map[string]any, error) +} + +func (opts AddSubportsOpts) ToTrunkAddSubportsMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +func AddSubports(ctx context.Context, c *gophercloud.ServiceClient, id string, opts AddSubportsOptsBuilder) (r UpdateSubportsResult) { + body, err := opts.ToTrunkAddSubportsMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, addSubportsURL(c, id), body, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +type RemoveSubport struct { + PortID string `json:"port_id" required:"true"` +} + +type RemoveSubportsOpts struct { + Subports []RemoveSubport `json:"sub_ports"` +} + +type RemoveSubportsOptsBuilder interface { + ToTrunkRemoveSubportsMap() (map[string]any, error) +} + +func (opts RemoveSubportsOpts) ToTrunkRemoveSubportsMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "") +} + +func RemoveSubports(ctx context.Context, c *gophercloud.ServiceClient, id string, opts RemoveSubportsOptsBuilder) (r UpdateSubportsResult) { + body, err := opts.ToTrunkRemoveSubportsMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Put(ctx, removeSubportsURL(c, id), body, &r.Body, &gophercloud.RequestOpts{ + OkCodes: []int{200}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/results.go new file mode 100644 index 000000000..72efd636f --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/results.go @@ -0,0 +1,142 @@ +package trunks + +import ( + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type Subport struct { + SegmentationID int `json:"segmentation_id" required:"true"` + SegmentationType string `json:"segmentation_type" required:"true"` + PortID string `json:"port_id" required:"true"` +} + +type commonResult struct { + gophercloud.Result +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Trunk. +type CreateResult struct { + commonResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Trunk. +type GetResult struct { + commonResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Trunk. +type UpdateResult struct { + commonResult +} + +// GetSubportsResult is the result of a Get request on the trunks subports +// resource. Call its Extract method to interpret it as a slice of Subport. +type GetSubportsResult struct { + commonResult +} + +// UpdateSubportsResult is the result of either an AddSubports or a RemoveSubports +// request. Call its Extract method to interpret it as a Trunk. +type UpdateSubportsResult struct { + commonResult +} + +type Trunk struct { + // Indicates whether the trunk is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', 'DEGRADED' or `ERROR'. + Status string `json:"status"` + + // A list of ports associated with the trunk + Subports []Subport `json:"sub_ports"` + + // Human-readable name for the trunk. Might not be unique. + Name string `json:"name,omitempty"` + + // The administrative state of the trunk. If false (down), the trunk does not + // forward packets. + AdminStateUp bool `json:"admin_state_up,omitempty"` + + // ProjectID is the project owner of the trunk. + ProjectID string `json:"project_id"` + + // TenantID is the project owner of the trunk. + TenantID string `json:"tenant_id"` + + // The date and time when the resource was created. + CreatedAt time.Time `json:"created_at"` + + // The date and time when the resource was updated, + // if the resource has not been updated, this field will show as null. + UpdatedAt time.Time `json:"updated_at"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // UUID of the trunk's parent port + PortID string `json:"port_id"` + + // UUID for the trunk resource + ID string `json:"id"` + + // Display description. + Description string `json:"description"` + + // A list of tags associated with the trunk + Tags []string `json:"tags,omitempty"` +} + +func (r commonResult) Extract() (*Trunk, error) { + var s struct { + Trunk *Trunk `json:"trunk"` + } + err := r.ExtractInto(&s) + return s.Trunk, err +} + +// TrunkPage is the page returned by a pager when traversing a collection of +// trunk resources. +type TrunkPage struct { + pagination.LinkedPageBase +} + +func (page TrunkPage) IsEmpty() (bool, error) { + if page.StatusCode == 204 { + return true, nil + } + + trunks, err := ExtractTrunks(page) + return len(trunks) == 0, err +} + +func ExtractTrunks(page pagination.Page) ([]Trunk, error) { + var a struct { + Trunks []Trunk `json:"trunks"` + } + err := (page.(TrunkPage)).ExtractInto(&a) + return a.Trunks, err +} + +func (r GetSubportsResult) Extract() ([]Subport, error) { + var s struct { + Subports []Subport `json:"sub_ports"` + } + err := r.ExtractInto(&s) + return s.Subports, err +} + +func (r UpdateSubportsResult) Extract() (t *Trunk, err error) { + err = r.ExtractInto(&t) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/urls.go new file mode 100644 index 000000000..477b85d7a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks/urls.go @@ -0,0 +1,45 @@ +package trunks + +import "github.com/gophercloud/gophercloud/v2" + +const resourcePath = "trunks" + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL(resourcePath) +} + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func getSubportsURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "get_subports") +} + +func addSubportsURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "add_subports") +} + +func removeSubportsURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL(resourcePath, id, "remove_subports") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/constants.go new file mode 100644 index 000000000..1214ce9de --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/constants.go @@ -0,0 +1,8 @@ +package networks + +const ( + StatusActive = "ACTIVE" + StatusBuild = "BUILD" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/doc.go new file mode 100644 index 000000000..4f8ce23ae --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/doc.go @@ -0,0 +1,66 @@ +/* +Package networks contains functionality for working with Neutron network +resources. A network is an isolated virtual layer-2 broadcast domain that is +typically reserved for the tenant who created it (unless you configure the +network to be shared). Tenants can create multiple networks until the +thresholds per-tenant quota is reached. + +In the v2.0 Networking API, the network is the main entity. Ports and subnets +are always associated with a network. + +Example to List Networks + + listOpts := networks.ListOpts{ + TenantID: "a99e9b4e620e4db09a2dfb6e42a01e66", + } + + allPages, err := networks.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allNetworks, err := networks.ExtractNetworks(allPages) + if err != nil { + panic(err) + } + + for _, network := range allNetworks { + fmt.Printf("%+v", network) + } + +Example to Create a Network + + iTrue := true + createOpts := networks.CreateOpts{ + Name: "network_1", + AdminStateUp: &iTrue, + } + + network, err := networks.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + + name := "new_name" + updateOpts := networks.UpdateOpts{ + Name: &name, + } + + network, err := networks.Update(context.TODO(), networkClient, networkID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Network + + networkID := "484cda0e-106f-4f4b-bb3f-d413710bbe78" + err := networks.Delete(context.TODO(), networkClient, networkID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package networks diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/requests.go new file mode 100644 index 000000000..d4dd64ff9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/requests.go @@ -0,0 +1,167 @@ +package networks + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToNetworkListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the network attributes you want to see returned. SortKey allows you to sort +// by a particular network attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + Shared *bool `q:"shared"` + ID string `q:"id"` + Marker string `q:"marker"` + Limit int `q:"limit"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` +} + +// ToNetworkListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToNetworkListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// networks. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToNetworkListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return NetworkPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific network based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToNetworkCreateMap() (map[string]any, error) +} + +// CreateOpts represents options used to create a network. +type CreateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Shared *bool `json:"shared,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + AvailabilityZoneHints []string `json:"availability_zone_hints,omitempty"` +} + +// ToNetworkCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToNetworkCreateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. This operation does not actually require a request body, i.e. the +// CreateOpts struct argument can be empty. +// +// The tenant ID that is contained in the URI is the tenant that creates the +// network. An admin user, however, has the option of specifying another tenant +// ID in the CreateOpts struct. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToNetworkCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, createURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToNetworkUpdateMap() (map[string]any, error) +} + +// UpdateOpts represents options used to update a network. +type UpdateOpts struct { + AdminStateUp *bool `json:"admin_state_up,omitempty"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + Shared *bool `json:"shared,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToNetworkUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToNetworkUpdateMap() (map[string]any, error) { + return gophercloud.BuildRequestBody(opts, "network") +} + +// Update accepts a UpdateOpts struct and updates an existing network using the +// values provided. For more information, see the Create function. +func Update(ctx context.Context, c *gophercloud.ServiceClient, networkID string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToNetworkUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(ctx, updateURL(c, networkID), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the network associated with it. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, networkID string) (r DeleteResult) { + resp, err := c.Delete(ctx, deleteURL(c, networkID), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/results.go new file mode 100644 index 000000000..53927bf0c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/results.go @@ -0,0 +1,177 @@ +package networks + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a network resource. +func (r commonResult) Extract() (*Network, error) { + var s Network + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v any) error { + return r.Result.ExtractIntoStructPtr(v, "network") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Network. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Network. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Network. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// Network represents, well, a network. +type Network struct { + // UUID for the network + ID string `json:"id"` + + // Human-readable name for the network. Might not be unique. + Name string `json:"name"` + + // Description for the network + Description string `json:"description"` + + // The administrative state of network. If false (down), the network does not + // forward packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Subnets associated with this network. + Subnets []string `json:"subnets"` + + // TenantID is the project owner of the network. + TenantID string `json:"tenant_id"` + + // UpdatedAt and CreatedAt contain ISO-8601 timestamps of when the state of the + // network last changed, and when it was created. + UpdatedAt time.Time `json:"-"` + CreatedAt time.Time `json:"-"` + + // ProjectID is the project owner of the network. + ProjectID string `json:"project_id"` + + // Specifies whether the network resource can be accessed by any tenant. + Shared bool `json:"shared"` + + // Availability zone hints groups network nodes that run services like DHCP, L3, FW, and others. + // Used to make network resources highly available. + AvailabilityZoneHints []string `json:"availability_zone_hints"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` +} + +func (r *Network) UnmarshalJSON(b []byte) error { + type tmp Network + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Network(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Network(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// NetworkPage is the page returned by a pager when traversing over a +// collection of networks. +type NetworkPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of networks has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r NetworkPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"networks_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a NetworkPage struct is empty. +func (r NetworkPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractNetworks(r) + return len(is) == 0, err +} + +// ExtractNetworks accepts a Page struct, specifically a NetworkPage struct, +// and extracts the elements into a slice of Network structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractNetworks(r pagination.Page) ([]Network, error) { + var s []Network + err := ExtractNetworksInto(r, &s) + return s, err +} + +func ExtractNetworksInto(r pagination.Page, v any) error { + return r.(NetworkPage).Result.ExtractIntoSlicePtr(v, "networks") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/urls.go new file mode 100644 index 000000000..14c352e39 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks/urls.go @@ -0,0 +1,31 @@ +package networks + +import "github.com/gophercloud/gophercloud/v2" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("networks", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("networks") +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/constants.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/constants.go new file mode 100644 index 000000000..6275839bf --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/constants.go @@ -0,0 +1,8 @@ +package ports + +const ( + StatusActive = "ACTIVE" + StatusBuild = "BUILD" + StatusDown = "DOWN" + StatusError = "ERROR" +) diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/doc.go new file mode 100644 index 000000000..025ad4fa9 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/doc.go @@ -0,0 +1,73 @@ +/* +Package ports contains functionality for working with Neutron port resources. + +A port represents a virtual switch port on a logical network switch. Virtual +instances attach their interfaces into ports. The logical port also defines +the MAC address and the IP address(es) to be assigned to the interfaces +plugged into them. When IP addresses are associated to a port, this also +implies the port is associated with a subnet, as the IP address was taken +from the allocation pool for a specific subnet. + +Example to List Ports + + listOpts := ports.ListOpts{ + DeviceID: "b0b89efe-82f8-461d-958b-adbf80f50c7d", + } + + allPages, err := ports.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allPorts, err := ports.ExtractPorts(allPages) + if err != nil { + panic(err) + } + + for _, port := range allPorts { + fmt.Printf("%+v\n", port) + } + +Example to Create a Port + + createOtps := ports.CreateOpts{ + Name: "private-port", + AdminStateUp: &asu, + NetworkID: "a87cc70a-3e15-4acf-8205-9b711a3531b7", + FixedIPs: []ports.IP{ + {SubnetID: "a0304c3a-4f08-4c43-88af-d796509c97d2", IPAddress: "10.0.0.2"}, + }, + SecurityGroups: &[]string{"foo"}, + AllowedAddressPairs: []ports.AddressPair{ + {IPAddress: "10.0.0.4", MACAddress: "fa:16:3e:c9:cb:f0"}, + }, + } + + port, err := ports.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + + updateOpts := ports.UpdateOpts{ + Name: "new_name", + SecurityGroups: &[]string{}, + } + + port, err := ports.Update(context.TODO(), networkClient, portID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Port + + portID := "c34bae2b-7641-49b6-bf6d-d8e473620ed8" + err := ports.Delete(context.TODO(), networkClient, portID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package ports diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/requests.go new file mode 100644 index 000000000..bfff2dffb --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/requests.go @@ -0,0 +1,247 @@ +package ports + +import ( + "context" + "fmt" + "net/url" + "slices" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToPortListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the port attributes you want to see returned. SortKey allows you to sort +// by a particular port attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Status string `q:"status"` + Name string `q:"name"` + Description string `q:"description"` + AdminStateUp *bool `q:"admin_state_up"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + DeviceOwner string `q:"device_owner"` + MACAddress string `q:"mac_address"` + ID string `q:"id"` + DeviceID string `q:"device_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` + SecurityGroups []string `q:"security_groups"` + FixedIPs []FixedIPOpts +} + +type FixedIPOpts struct { + IPAddress string + IPAddressSubstr string + SubnetID string +} + +func (f FixedIPOpts) toParams() []string { + var res []string + if f.IPAddress != "" { + res = append(res, fmt.Sprintf("ip_address=%s", f.IPAddress)) + } + if f.IPAddressSubstr != "" { + res = append(res, fmt.Sprintf("ip_address_substr=%s", f.IPAddressSubstr)) + } + if f.SubnetID != "" { + res = append(res, fmt.Sprintf("subnet_id=%s", f.SubnetID)) + } + return res +} + +// ToPortListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToPortListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + params := q.Query() + for _, fixedIP := range opts.FixedIPs { + for _, fixedIPParam := range fixedIP.toParams() { + params.Add("fixed_ips", fixedIPParam) + } + } + q = &url.URL{RawQuery: params.Encode()} + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// ports. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those ports that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToPortListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return PortPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific port based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToPortCreateMap() (map[string]any, error) +} + +// CreateOpts represents the attributes used when creating a new port. +type CreateOpts struct { + NetworkID string `json:"network_id" required:"true"` + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + MACAddress string `json:"mac_address,omitempty"` + FixedIPs any `json:"fixed_ips,omitempty"` + DeviceID string `json:"device_id,omitempty"` + DeviceOwner string `json:"device_owner,omitempty"` + TenantID string `json:"tenant_id,omitempty"` + ProjectID string `json:"project_id,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs,omitempty"` + PropagateUplinkStatus *bool `json:"propagate_uplink_status,omitempty"` + ValueSpecs *map[string]string `json:"value_specs,omitempty"` +} + +// ToPortCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToPortCreateMap() (map[string]any, error) { + body, err := gophercloud.BuildRequestBody(opts, "port") + if err != nil { + return nil, err + } + + return AddValueSpecs(body) +} + +// AddValueSpecs expands the 'value_specs' object and removes 'value_specs' +// from the request body. It will return error if the value specs would overwrite +// an existing field or contains forbidden keys. +func AddValueSpecs(body map[string]any) (map[string]any, error) { + // Banned the same as in heat. See https://github.com/openstack/heat/blob/dd7319e373b88812cb18897f742b5196a07227ea/heat/engine/resources/openstack/neutron/neutron.py#L59 + bannedKeys := []string{"shared", "tenant_id"} + port := body["port"].(map[string]any) + + if port["value_specs"] != nil { + for k, v := range port["value_specs"].(map[string]any) { + if slices.Contains(bannedKeys, k) { + return nil, fmt.Errorf("forbidden key in value_specs: %s", k) + } + if _, ok := port[k]; ok { + return nil, fmt.Errorf("value_specs would overwrite key: %s", k) + } + port[k] = v + } + delete(port, "value_specs") + } + body["port"] = port + + return body, nil +} + +// Create accepts a CreateOpts struct and creates a new network using the values +// provided. You must remember to provide a NetworkID value. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToPortCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, createURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToPortUpdateMap() (map[string]any, error) +} + +// UpdateOpts represents the attributes used when updating an existing port. +type UpdateOpts struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + AdminStateUp *bool `json:"admin_state_up,omitempty"` + FixedIPs any `json:"fixed_ips,omitempty"` + DeviceID *string `json:"device_id,omitempty"` + DeviceOwner *string `json:"device_owner,omitempty"` + SecurityGroups *[]string `json:"security_groups,omitempty"` + AllowedAddressPairs *[]AddressPair `json:"allowed_address_pairs,omitempty"` + PropagateUplinkStatus *bool `json:"propagate_uplink_status,omitempty"` + ValueSpecs *map[string]string `json:"value_specs,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` +} + +// ToPortUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToPortUpdateMap() (map[string]any, error) { + body, err := gophercloud.BuildRequestBody(opts, "port") + if err != nil { + return nil, err + } + return AddValueSpecs(body) +} + +// Update accepts a UpdateOpts struct and updates an existing port using the +// values provided. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToPortUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + resp, err := c.Put(ctx, updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the port associated with it. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, deleteURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/results.go new file mode 100644 index 000000000..db223d48c --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/results.go @@ -0,0 +1,206 @@ +package ports + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a port resource. +func (r commonResult) Extract() (*Port, error) { + var s Port + err := r.ExtractInto(&s) + return &s, err +} + +func (r commonResult) ExtractInto(v any) error { + return r.Result.ExtractIntoStructPtr(v, "port") +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Port. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Port. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Port. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// IP is a sub-struct that represents an individual IP. +type IP struct { + SubnetID string `json:"subnet_id,omitempty"` + IPAddress string `json:"ip_address,omitempty"` +} + +// AddressPair contains the IP Address and the MAC address. +type AddressPair struct { + IPAddress string `json:"ip_address,omitempty"` + MACAddress string `json:"mac_address,omitempty"` +} + +// Port represents a Neutron port. See package documentation for a top-level +// description of what this is. +type Port struct { + // UUID for the port. + ID string `json:"id"` + + // Network that this port is associated with. + NetworkID string `json:"network_id"` + + // Human-readable name for the port. Might not be unique. + Name string `json:"name"` + + // Describes the port. + Description string `json:"description"` + + // Administrative state of port. If false (down), port does not forward + // packets. + AdminStateUp bool `json:"admin_state_up"` + + // Indicates whether network is currently operational. Possible values include + // `ACTIVE', `DOWN', `BUILD', or `ERROR'. Plug-ins might define additional + // values. + Status string `json:"status"` + + // Mac address to use on this port. + MACAddress string `json:"mac_address"` + + // Specifies IP addresses for the port thus associating the port itself with + // the subnets where the IP addresses are picked from + FixedIPs []IP `json:"fixed_ips"` + + // TenantID is the project owner of the port. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the port. + ProjectID string `json:"project_id"` + + // Identifies the entity (e.g.: dhcp agent) using this port. + DeviceOwner string `json:"device_owner"` + + // Specifies the IDs of any security groups associated with a port. + SecurityGroups []string `json:"security_groups"` + + // Identifies the device (e.g., virtual server) using this port. + DeviceID string `json:"device_id"` + + // Identifies the list of IP addresses the port will recognize/accept + AllowedAddressPairs []AddressPair `json:"allowed_address_pairs"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // PropagateUplinkStatus enables/disables propagate uplink status on the port. + PropagateUplinkStatus bool `json:"propagate_uplink_status"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // Timestamp when the port was created + CreatedAt time.Time `json:"created_at"` + + // Timestamp when the port was last updated + UpdatedAt time.Time `json:"updated_at"` +} + +func (r *Port) UnmarshalJSON(b []byte) error { + type tmp Port + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Port(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Port(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// PortPage is the page returned by a pager when traversing over a collection +// of network ports. +type PortPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of ports has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r PortPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"ports_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a PortPage struct is empty. +func (r PortPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractPorts(r) + return len(is) == 0, err +} + +// ExtractPorts accepts a Page struct, specifically a PortPage struct, +// and extracts the elements into a slice of Port structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractPorts(r pagination.Page) ([]Port, error) { + var s []Port + err := ExtractPortsInto(r, &s) + return s, err +} + +func ExtractPortsInto(r pagination.Page, v any) error { + return r.(PortPage).Result.ExtractIntoSlicePtr(v, "ports") +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/urls.go new file mode 100644 index 000000000..e52f44f65 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports/urls.go @@ -0,0 +1,31 @@ +package ports + +import "github.com/gophercloud/gophercloud/v2" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("ports", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("ports") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/doc.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/doc.go new file mode 100644 index 000000000..eef6afd53 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/doc.go @@ -0,0 +1,138 @@ +/* +Package subnets contains functionality for working with Neutron subnet +resources. A subnet represents an IP address block that can be used to +assign IP addresses to virtual instances. Each subnet must have a CIDR and +must be associated with a network. IPs can either be selected from the whole +subnet CIDR or from allocation pools specified by the user. + +A subnet can also have a gateway, a list of DNS name servers, and host routes. +This information is pushed to instances whose interfaces are associated with +the subnet. + +Example to List Subnets + + listOpts := subnets.ListOpts{ + IPVersion: 4, + } + + allPages, err := subnets.List(networkClient, listOpts).AllPages(context.TODO()) + if err != nil { + panic(err) + } + + allSubnets, err := subnets.ExtractSubnets(allPages) + if err != nil { + panic(err) + } + + for _, subnet := range allSubnets { + fmt.Printf("%+v\n", subnet) + } + +Example to Create a Subnet With Specified Gateway + + var gatewayIP = "192.168.199.1" + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a22", + IPVersion: 4, + CIDR: "192.168.199.0/24", + GatewayIP: &gatewayIP, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.199.2", + End: "192.168.199.254", + }, + }, + DNSNameservers: []string{"foo"}, + ServiceTypes: []string{"network:floatingip"}, + } + + subnet, err := subnets.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With No Gateway + + var noGateway = "" + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + GatewayIP: &noGateway, + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Create a Subnet With a Default Gateway + + createOpts := subnets.CreateOpts{ + NetworkID: "d32019d3-bc6e-4319-9c1d-6722fc136a23", + IPVersion: 4, + CIDR: "192.168.1.0/24", + AllocationPools: []subnets.AllocationPool{ + { + Start: "192.168.1.2", + End: "192.168.1.254", + }, + }, + DNSNameservers: []string{}, + } + + subnet, err := subnets.Create(context.TODO(), networkClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + dnsNameservers := []string{"8.8.8.8"} + serviceTypes := []string{"network:floatingip", "network:routed"} + name := "new_name" + + updateOpts := subnets.UpdateOpts{ + Name: &name, + DNSNameservers: &dnsNameservers, + ServiceTypes: &serviceTypes, + } + + subnet, err := subnets.Update(context.TODO(), networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Remove a Gateway From a Subnet + + var noGateway = "" + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + + updateOpts := subnets.UpdateOpts{ + GatewayIP: &noGateway, + } + + subnet, err := subnets.Update(context.TODO(), networkClient, subnetID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Subnet + + subnetID := "db77d064-e34f-4d06-b060-f21e28a61c23" + err := subnets.Delete(context.TODO(), networkClient, subnetID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package subnets diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/requests.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/requests.go new file mode 100644 index 000000000..85c5d2b40 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/requests.go @@ -0,0 +1,278 @@ +package subnets + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to the +// List request. +type ListOptsBuilder interface { + ToSubnetListQuery() (string, error) +} + +// ListOpts allows the filtering and sorting of paginated collections through +// the API. Filtering is achieved by passing in struct field values that map to +// the subnet attributes you want to see returned. SortKey allows you to sort +// by a particular subnet attribute. SortDir sets the direction, and is either +// `asc' or `desc'. Marker and Limit are used for pagination. +type ListOpts struct { + Name string `q:"name"` + Description string `q:"description"` + DNSPublishFixedIP *bool `q:"dns_publish_fixed_ip"` + EnableDHCP *bool `q:"enable_dhcp"` + NetworkID string `q:"network_id"` + TenantID string `q:"tenant_id"` + ProjectID string `q:"project_id"` + IPVersion int `q:"ip_version"` + GatewayIP string `q:"gateway_ip"` + CIDR string `q:"cidr"` + IPv6AddressMode string `q:"ipv6_address_mode"` + IPv6RAMode string `q:"ipv6_ra_mode"` + ID string `q:"id"` + SubnetPoolID string `q:"subnetpool_id"` + Limit int `q:"limit"` + Marker string `q:"marker"` + SortKey string `q:"sort_key"` + SortDir string `q:"sort_dir"` + Tags string `q:"tags"` + TagsAny string `q:"tags-any"` + NotTags string `q:"not-tags"` + NotTagsAny string `q:"not-tags-any"` + RevisionNumber *int `q:"revision_number"` + SegmentID string `q:"segment_id"` +} + +// ToSubnetListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToSubnetListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// List returns a Pager which allows you to iterate over a collection of +// subnets. It accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +// +// Default policy settings return only those subnets that are owned by the tenant +// who submits the request, unless the request is submitted by a user with +// administrative rights. +func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(c) + if opts != nil { + query, err := opts.ToSubnetListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(c, url, func(r pagination.PageResult) pagination.Page { + return SubnetPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves a specific subnet based on its unique ID. +func Get(ctx context.Context, c *gophercloud.ServiceClient, id string) (r GetResult) { + resp, err := c.Get(ctx, getURL(c, id), &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// List request. +type CreateOptsBuilder interface { + ToSubnetCreateMap() (map[string]any, error) +} + +// CreateOpts represents the attributes used when creating a new subnet. +type CreateOpts struct { + // NetworkID is the UUID of the network the subnet will be associated with. + NetworkID string `json:"network_id" required:"true"` + + // CIDR is the address CIDR of the subnet. + CIDR string `json:"cidr,omitempty"` + + // Name is a human-readable name of the subnet. + Name string `json:"name,omitempty"` + + // Description of the subnet. + Description string `json:"description,omitempty"` + + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + TenantID string `json:"tenant_id,omitempty"` + + // The UUID of the project who owns the Subnet. Only administrative users + // can specify a project UUID other than their own. + ProjectID string `json:"project_id,omitempty"` + + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to nil will + // cause a default gateway to automatically be created. Setting to an empty + // string will cause the subnet to be created with no gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // IPVersion is the IP version for the subnet. + IPVersion gophercloud.IPVersion `json:"ip_version,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers []string `json:"dns_nameservers,omitempty"` + + // DNSPublishFixedIP will either enable or disable the publication of fixed IPs to the DNS + DNSPublishFixedIP *bool `json:"dns_publish_fixed_ip,omitempty"` + + // ServiceTypes are the service types associated with the subnet. + ServiceTypes []string `json:"service_types,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes []HostRoute `json:"host_routes,omitempty"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode,omitempty"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode,omitempty"` + + // SubnetPoolID is the id of the subnet pool that subnet should be associated to. + SubnetPoolID string `json:"subnetpool_id,omitempty"` + + // Prefixlen is used when user creates a subnet from the subnetpool. It will + // overwrite the "default_prefixlen" value of the referenced subnetpool. + Prefixlen int `json:"prefixlen,omitempty"` + + // SegmentID is a network segment the subnet is associated with. It is + // available when segment extension is enabled. + SegmentID string `json:"segment_id,omitempty"` +} + +// ToSubnetCreateMap builds a request body from CreateOpts. +func (opts CreateOpts) ToSubnetCreateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]any); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Create accepts a CreateOpts struct and creates a new subnet using the values +// provided. You must remember to provide a valid NetworkID, CIDR and IP +// version. +func Create(ctx context.Context, c *gophercloud.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToSubnetCreateMap() + if err != nil { + r.Err = err + return + } + resp, err := c.Post(ctx, createURL(c), b, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToSubnetUpdateMap() (map[string]any, error) +} + +// UpdateOpts represents the attributes used when updating an existing subnet. +type UpdateOpts struct { + // Name is a human-readable name of the subnet. + Name *string `json:"name,omitempty"` + + // Description of the subnet. + Description *string `json:"description,omitempty"` + + // AllocationPools are IP Address pools that will be available for DHCP. + AllocationPools []AllocationPool `json:"allocation_pools,omitempty"` + + // GatewayIP sets gateway information for the subnet. Setting to an empty + // string will cause the subnet to not have a gateway. Setting to + // an explicit address will set that address as the gateway. + GatewayIP *string `json:"gateway_ip,omitempty"` + + // DNSNameservers are the nameservers to be set via DHCP. + DNSNameservers *[]string `json:"dns_nameservers,omitempty"` + + // DNSPublishFixedIP will either enable or disable the publication of fixed IPs to the DNS + DNSPublishFixedIP *bool `json:"dns_publish_fixed_ip,omitempty"` + + // ServiceTypes are the service types associated with the subnet. + ServiceTypes *[]string `json:"service_types,omitempty"` + + // HostRoutes are any static host routes to be set via DHCP. + HostRoutes *[]HostRoute `json:"host_routes,omitempty"` + + // EnableDHCP will either enable to disable the DHCP service. + EnableDHCP *bool `json:"enable_dhcp,omitempty"` + + // RevisionNumber implements extension:standard-attr-revisions. If != "" it + // will set revision_number=%s. If the revision number does not match, the + // update will fail. + RevisionNumber *int `json:"-" h:"If-Match"` + + // SegmentID is a network segment the subnet is associated with. It is + // available when segment extension is enabled. + SegmentID *string `json:"segment_id,omitempty"` +} + +// ToSubnetUpdateMap builds a request body from UpdateOpts. +func (opts UpdateOpts) ToSubnetUpdateMap() (map[string]any, error) { + b, err := gophercloud.BuildRequestBody(opts, "subnet") + if err != nil { + return nil, err + } + + if m := b["subnet"].(map[string]any); m["gateway_ip"] == "" { + m["gateway_ip"] = nil + } + + return b, nil +} + +// Update accepts a UpdateOpts struct and updates an existing subnet using the +// values provided. +func Update(ctx context.Context, c *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToSubnetUpdateMap() + if err != nil { + r.Err = err + return + } + h, err := gophercloud.BuildHeaders(opts) + if err != nil { + r.Err = err + return + } + for k := range h { + if k == "If-Match" { + h[k] = fmt.Sprintf("revision_number=%s", h[k]) + } + } + + resp, err := c.Put(ctx, updateURL(c, id), b, &r.Body, &gophercloud.RequestOpts{ + MoreHeaders: h, + OkCodes: []int{200, 201}, + }) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// Delete accepts a unique ID and deletes the subnet associated with it. +func Delete(ctx context.Context, c *gophercloud.ServiceClient, id string) (r DeleteResult) { + resp, err := c.Delete(ctx, deleteURL(c, id), nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/results.go new file mode 100644 index 000000000..4f0aa8408 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/results.go @@ -0,0 +1,216 @@ +package subnets + +import ( + "encoding/json" + "time" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/pagination" +) + +type commonResult struct { + gophercloud.Result +} + +// Extract is a function that accepts a result and extracts a subnet resource. +func (r commonResult) Extract() (*Subnet, error) { + var s struct { + Subnet *Subnet `json:"subnet"` + } + err := r.ExtractInto(&s) + return s.Subnet, err +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Subnet. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Subnet. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Subnet. +type UpdateResult struct { + commonResult +} + +// DeleteResult represents the result of a delete operation. Call its +// ExtractErr method to determine if the request succeeded or failed. +type DeleteResult struct { + gophercloud.ErrResult +} + +// AllocationPool represents a sub-range of cidr available for dynamic +// allocation to ports, e.g. {Start: "10.0.0.2", End: "10.0.0.254"} +type AllocationPool struct { + Start string `json:"start"` + End string `json:"end"` +} + +// HostRoute represents a route that should be used by devices with IPs from +// a subnet (not including local subnet route). +type HostRoute struct { + DestinationCIDR string `json:"destination"` + NextHop string `json:"nexthop"` +} + +// Subnet represents a subnet. See package documentation for a top-level +// description of what this is. +type Subnet struct { + // UUID representing the subnet. + ID string `json:"id"` + + // UUID of the parent network. + NetworkID string `json:"network_id"` + + // Human-readable name for the subnet. Might not be unique. + Name string `json:"name"` + + // Description for the subnet. + Description string `json:"description"` + + // IP version, either `4' or `6'. + IPVersion int `json:"ip_version"` + + // CIDR representing IP range for this subnet, based on IP version. + CIDR string `json:"cidr"` + + // Default gateway used by devices in this subnet. + GatewayIP string `json:"gateway_ip"` + + // DNS name servers used by hosts in this subnet. + DNSNameservers []string `json:"dns_nameservers"` + + // Specifies whether the fixed IP addresses are published to the DNS. + DNSPublishFixedIP bool `json:"dns_publish_fixed_ip"` + + // Service types associated with the subnet. + ServiceTypes []string `json:"service_types"` + + // Sub-ranges of CIDR available for dynamic allocation to ports. + // See AllocationPool. + AllocationPools []AllocationPool `json:"allocation_pools"` + + // Routes that should be used by devices with IPs from this subnet + // (not including local subnet route). + HostRoutes []HostRoute `json:"host_routes"` + + // Specifies whether DHCP is enabled for this subnet or not. + EnableDHCP bool `json:"enable_dhcp"` + + // TenantID is the project owner of the subnet. + TenantID string `json:"tenant_id"` + + // ProjectID is the project owner of the subnet. + ProjectID string `json:"project_id"` + + // The IPv6 address modes specifies mechanisms for assigning IPv6 IP addresses. + IPv6AddressMode string `json:"ipv6_address_mode"` + + // The IPv6 router advertisement specifies whether the networking service + // should transmit ICMPv6 packets. + IPv6RAMode string `json:"ipv6_ra_mode"` + + // SubnetPoolID is the id of the subnet pool associated with the subnet. + SubnetPoolID string `json:"subnetpool_id"` + + // Tags optionally set via extensions/attributestags + Tags []string `json:"tags"` + + // RevisionNumber optionally set via extensions/standard-attr-revisions + RevisionNumber int `json:"revision_number"` + + // SegmentID of a network segment the subnet is associated with. It is + // available when segment extension is enabled. + SegmentID string `json:"segment_id"` + + // Timestamp when the subnet was created + CreatedAt time.Time `json:"-"` + + // Timestamp when the subnet was last updated + UpdatedAt time.Time `json:"-"` +} + +func (r *Subnet) UnmarshalJSON(b []byte) error { + type tmp Subnet + + // Support for older neutron time format + var s1 struct { + tmp + CreatedAt gophercloud.JSONRFC3339NoZ `json:"created_at"` + UpdatedAt gophercloud.JSONRFC3339NoZ `json:"updated_at"` + } + + err := json.Unmarshal(b, &s1) + if err == nil { + *r = Subnet(s1.tmp) + r.CreatedAt = time.Time(s1.CreatedAt) + r.UpdatedAt = time.Time(s1.UpdatedAt) + + return nil + } + + // Support for newer neutron time format + var s2 struct { + tmp + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + } + + err = json.Unmarshal(b, &s2) + if err != nil { + return err + } + + *r = Subnet(s2.tmp) + r.CreatedAt = time.Time(s2.CreatedAt) + r.UpdatedAt = time.Time(s2.UpdatedAt) + + return nil +} + +// SubnetPage is the page returned by a pager when traversing over a collection +// of subnets. +type SubnetPage struct { + pagination.LinkedPageBase +} + +// NextPageURL is invoked when a paginated collection of subnets has reached +// the end of a page and the pager seeks to traverse over a new one. In order +// to do this, it needs to construct the next page's URL. +func (r SubnetPage) NextPageURL() (string, error) { + var s struct { + Links []gophercloud.Link `json:"subnets_links"` + } + err := r.ExtractInto(&s) + if err != nil { + return "", err + } + return gophercloud.ExtractNextURL(s.Links) +} + +// IsEmpty checks whether a SubnetPage struct is empty. +func (r SubnetPage) IsEmpty() (bool, error) { + if r.StatusCode == 204 { + return true, nil + } + + is, err := ExtractSubnets(r) + return len(is) == 0, err +} + +// ExtractSubnets accepts a Page struct, specifically a SubnetPage struct, +// and extracts the elements into a slice of Subnet structs. In other words, +// a generic collection is mapped into a relevant slice. +func ExtractSubnets(r pagination.Page) ([]Subnet, error) { + var s struct { + Subnets []Subnet `json:"subnets"` + } + err := (r.(SubnetPage)).ExtractInto(&s) + return s.Subnets, err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/urls.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/urls.go new file mode 100644 index 000000000..e4ad4dd0e --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets/urls.go @@ -0,0 +1,31 @@ +package subnets + +import "github.com/gophercloud/gophercloud/v2" + +func resourceURL(c *gophercloud.ServiceClient, id string) string { + return c.ServiceURL("subnets", id) +} + +func rootURL(c *gophercloud.ServiceClient) string { + return c.ServiceURL("subnets") +} + +func listURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func getURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func createURL(c *gophercloud.ServiceClient) string { + return rootURL(c) +} + +func updateURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} + +func deleteURL(c *gophercloud.ServiceClient, id string) string { + return resourceURL(c, id) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/base_endpoint.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/base_endpoint.go new file mode 100644 index 000000000..f219c0bf4 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/base_endpoint.go @@ -0,0 +1,41 @@ +package utils + +import ( + "net/url" + "regexp" + "strings" +) + +func parseEndpoint(endpoint string, includeVersion bool) (string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", err + } + + u.RawQuery, u.Fragment = "", "" + + path := u.Path + versionRe := regexp.MustCompile("v[0-9.]+/?") + + if version := versionRe.FindString(path); version != "" { + versionIndex := strings.Index(path, version) + if includeVersion { + versionIndex += len(version) + } + u.Path = path[:versionIndex] + } + + return u.String(), nil +} + +// BaseEndpoint will return a URL without the /vX.Y +// portion of the URL. +func BaseEndpoint(endpoint string) (string, error) { + return parseEndpoint(endpoint, false) +} + +// BaseVersionedEndpoint will return a URL with the /vX.Y portion of the URL, +// if present, but without a project ID or similar +func BaseVersionedEndpoint(endpoint string) (string, error) { + return parseEndpoint(endpoint, true) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/choose_version.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/choose_version.go new file mode 100644 index 000000000..ccc56345a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/choose_version.go @@ -0,0 +1,116 @@ +package utils + +import ( + "context" + "fmt" + "strings" + + "github.com/gophercloud/gophercloud/v2" +) + +// Version is a supported API version, corresponding to a vN package within the appropriate service. +type Version struct { + ID string + Suffix string + Priority int +} + +var goodStatus = map[string]bool{ + "current": true, + "supported": true, + "stable": true, +} + +// ChooseVersion queries the base endpoint of an API to choose the identity service version. +// It will pick a version among the recognized, taking into account the priority and avoiding +// experimental alternatives from the published versions. However, if the client specifies a full +// endpoint that is among the recognized versions, it will be used regardless of priority. +// It returns the highest-Priority Version, OR exact match with client endpoint, +// among the alternatives that are provided, as well as its corresponding endpoint. +func ChooseVersion(ctx context.Context, client *gophercloud.ProviderClient, recognized []*Version) (*Version, string, error) { + // TODO(stephenfin): This could be removed since we can accomplish this with GetServiceVersions now. + type linkResp struct { + Href string `json:"href"` + Rel string `json:"rel"` + } + + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Links []linkResp `json:"links"` + } + + type versionsResp struct { + Values []valueResp `json:"values"` + } + + type response struct { + Versions versionsResp `json:"versions"` + } + + normalize := func(endpoint string) string { + if !strings.HasSuffix(endpoint, "/") { + return endpoint + "/" + } + return endpoint + } + identityEndpoint := normalize(client.IdentityEndpoint) + + // If a full endpoint is specified, check version suffixes for a match first. + for _, v := range recognized { + if strings.HasSuffix(identityEndpoint, v.Suffix) { + return v, identityEndpoint, nil + } + } + + var resp response + _, err := client.Request(ctx, "GET", client.IdentityBase, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + + if err != nil { + return nil, "", err + } + + var highest *Version + var endpoint string + + for _, value := range resp.Versions.Values { + href := "" + for _, link := range value.Links { + if link.Rel == "self" { + href = normalize(link.Href) + } + } + + for _, version := range recognized { + if strings.Contains(value.ID, version.ID) { + // Prefer a version that exactly matches the provided endpoint. + if href == identityEndpoint { + if href == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", value.ID, client.IdentityBase) + } + return version, href, nil + } + + // Otherwise, find the highest-priority version with a whitelisted status. + if goodStatus[strings.ToLower(value.Status)] { + if highest == nil || version.Priority > highest.Priority { + highest = version + endpoint = href + } + } + } + } + } + + if highest == nil { + return nil, "", fmt.Errorf("No supported version available from endpoint %s", client.IdentityBase) + } + if endpoint == "" { + return nil, "", fmt.Errorf("Endpoint missing in version %s response from %s", highest.ID, client.IdentityBase) + } + + return highest, endpoint, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/discovery.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/discovery.go new file mode 100644 index 000000000..86d1d14c3 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/utils/discovery.go @@ -0,0 +1,372 @@ +package utils + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud/v2" +) + +type Status string + +const ( + StatusCurrent Status = "CURRENT" + StatusSupported Status = "SUPPORTED" + StatusDeprecated Status = "DEPRECATED" + StatusExperimental Status = "EXPERIMENTAL" + StatusUnknown Status = "" +) + +// SupportedVersion stores a normalized form of the API version data. It handles APIs that +// support microversions as well as those that do not. +type SupportedVersion struct { + // Major is the major version number of the API + Major int + // Minor is the minor version number of the API + Minor int + // Status is the status of the API + Status Status + SupportedMicroversions +} + +// SupportedMicroversions stores a normalized form of the maximum and minimum API microversions +// supported by a given service. +type SupportedMicroversions struct { + // MaxMajor is the major version number of the maximum supported API microversion + MaxMajor int + // MaxMinor is the minor version number of the maximum supported API microversion + MaxMinor int + // MinMajor is the major version number of the minimum supported API microversion + MinMajor int + // MinMinor is the minor version number of the minimum supported API microversion + MinMinor int +} + +type version struct { + ID string `json:"id"` + Status string `json:"status"` + Version string `json:"version,omitempty"` + MaxVersion string `json:"max_version,omitempty"` + MinVersion string `json:"min_version"` +} + +type response struct { + Versions []version `json:"-"` +} + +func (r *response) UnmarshalJSON(in []byte) error { + // intermediateResponse is an intermediate struct that allows us to offload the difference + // between a single version document and a multi-version document to the json parser and + // only focus on differences in the latter + type intermediateResponse struct { + ID string `json:"id"` + Version *version `json:"version"` + Versions *json.RawMessage `json:"versions"` + } + + data := intermediateResponse{} + if err := json.Unmarshal(in, &data); err != nil { + return err + } + + // case 1: we have a single enveloped version object + // + // this is the approach used by Manila for single version responses + if data.Version != nil { + r.Versions = []version{*data.Version} + return nil + } + + // case 2: we have an singly enveloped array of version objects + // + // this is the approach used by nova, cinder and glance, among others, for multi-version + // responses + if data.Versions != nil { + var versionArr []version + if err := json.Unmarshal(*data.Versions, &versionArr); err == nil { + r.Versions = versionArr + return nil + } + } + + // case 3: we have an doubly enveloped array of version objects + // + // this is the approach used by keystone and barbican, among others, for multi-version + // responses + if data.Versions != nil { + type values struct { + Values []version `json:"values"` + } + + var valuesObj values + if err := json.Unmarshal(*data.Versions, &valuesObj); err == nil { + r.Versions = valuesObj.Values + return nil + } + } + + // case 4: we have a single unenveloped version object + // + // this is the approach used by most other services for single version responses + if data.ID != "" { + r.Versions = []version{{ID: data.ID}} + return nil + } + + return fmt.Errorf("failed to unmarshal versions document: %s", in) +} + +func extractVersion(endpointURL string) (int, int, error) { + u, err := url.Parse(endpointURL) + if err != nil { + return 0, 0, err + } + + parts := strings.Split(strings.TrimRight(u.Path, "/"), "/") + if len(parts) == 0 { + return 0, 0, fmt.Errorf("expected path with version, got: %s", u.Path) + } + + // first, check the nth path element for a version string + if majorVersion, minorVersion, err := ParseVersion(parts[len(parts)-1]); err == nil { + return majorVersion, minorVersion, nil + } + + // if there are no more parts, quit + if len(parts) == 1 { + // we don't return the error message directly since it might be misleading: at this point + // we might have a *malformed* version identifier rather than *no* version identifier + return 0, 0, fmt.Errorf("failed to infer version from path: %s", u.Path) + } + + // the guidelines say we should use the currently scoped project_id from the token, but we + // don't necessarily have a token yet so we speculatively look at the (n-1)th path element + // (but only that) just as keystoneauth does + // + // https://github.com/openstack/keystoneauth/blob/master/keystoneauth1/discover.py#L1534-L1545 + if majorVersion, minorVersion, err := ParseVersion(parts[len(parts)-1]); err == nil { + return majorVersion, minorVersion, err + } + + // once again, we don't return the error message directly + return 0, 0, fmt.Errorf("failed to infer version from path: %s", u.Path) +} + +// GetServiceVersions returns the versions supported by the ServiceClient Endpoint. +// If the endpoint resolves to an unversioned discovery API, this should return one or more supported versions. +// If the endpoint resolves to a versioned discovery API, this should return exactly one supported version. +func GetServiceVersions(ctx context.Context, client *gophercloud.ProviderClient, endpointURL string, discoverVersions bool) ([]SupportedVersion, error) { + var supportedVersions []SupportedVersion + var endpointVersion *SupportedVersion + + if majorVersion, minorVersion, err := extractVersion(endpointURL); err == nil { + endpointVersion = &SupportedVersion{Major: majorVersion, Minor: minorVersion} + if !discoverVersions { + return append(supportedVersions, *endpointVersion), nil + } + } + + var resp response + _, err := client.Request(ctx, "GET", endpointURL, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + if err != nil { + // we weren't able to find a discovery document but we have version information from the URL + if endpointVersion != nil { + return append(supportedVersions, *endpointVersion), nil + } + return supportedVersions, err + } + + versions := resp.Versions + + for _, version := range versions { + majorVersion, minorVersion, err := ParseVersion(version.ID) + if err != nil { + return supportedVersions, err + } + + status, err := ParseStatus(version.Status) + if err != nil { + return supportedVersions, err + } + + supportedVersion := SupportedVersion{ + Major: majorVersion, + Minor: minorVersion, + Status: status, + } + + // Only normalize the microversions if there are microversions to normalize + if (version.Version != "" || version.MaxVersion != "") && version.MinVersion != "" { + supportedVersion.MinMajor, supportedVersion.MinMinor, err = ParseMicroversion(version.MinVersion) + if err != nil { + return supportedVersions, err + } + + maxVersion := version.Version + if maxVersion == "" { + maxVersion = version.MaxVersion + } + supportedVersion.MaxMajor, supportedVersion.MaxMinor, err = ParseMicroversion(maxVersion) + if err != nil { + return supportedVersions, err + } + } + + supportedVersions = append(supportedVersions, supportedVersion) + } + + sort.Slice(supportedVersions, func(i, j int) bool { + return supportedVersions[i].Major > supportedVersions[j].Major || (supportedVersions[i].Major == supportedVersions[j].Major && + supportedVersions[i].Minor > supportedVersions[j].Minor) + }) + + return supportedVersions, nil +} + +// GetSupportedMicroversions returns the minimum and maximum microversion that is supported by the ServiceClient Endpoint. +func GetSupportedMicroversions(ctx context.Context, client *gophercloud.ServiceClient) (SupportedMicroversions, error) { + var supportedMicroversions SupportedMicroversions + + supportedVersions, err := GetServiceVersions(ctx, client.ProviderClient, client.Endpoint, true) + if err != nil { + return supportedMicroversions, err + } + + // If there are multiple versions then we were handed an unversioned endpoint. These don't + // provide microversion information, so we need to fail. Likewise, if there are no versions + // then something has gone wrong and we also need to fail. + if len(supportedVersions) > 1 { + return supportedMicroversions, fmt.Errorf("unversioned endpoint with multiple alternatives not supported") + } else if len(supportedVersions) == 0 { + return supportedMicroversions, fmt.Errorf("microversions not supported by endpoint") + } + + supportedMicroversions = supportedVersions[0].SupportedMicroversions + + if supportedMicroversions.MaxMajor == 0 && + supportedMicroversions.MaxMinor == 0 && + supportedMicroversions.MinMajor == 0 && + supportedMicroversions.MinMinor == 0 { + return supportedMicroversions, fmt.Errorf("microversions not supported by endpoint") + } + + return supportedMicroversions, err +} + +// RequireMicroversion checks that the required microversion is supported and +// returns a ServiceClient with the microversion set. +func RequireMicroversion(ctx context.Context, client gophercloud.ServiceClient, required string) (gophercloud.ServiceClient, error) { + supportedMicroversions, err := GetSupportedMicroversions(ctx, &client) + if err != nil { + return client, fmt.Errorf("unable to determine supported microversions: %w", err) + } + supported, err := supportedMicroversions.IsSupported(required) + if err != nil { + return client, err + } + if !supported { + return client, fmt.Errorf("microversion %s not supported. Supported versions: %v", required, supportedMicroversions) + } + client.Microversion = required + return client, nil +} + +// IsSupported checks if a microversion falls in the supported interval. +// It returns true if the version is within the interval and false otherwise. +func (supported SupportedMicroversions) IsSupported(version string) (bool, error) { + // Parse the version X.Y into X and Y integers that are easier to compare. + vMajor, vMinor, err := ParseMicroversion(version) + if err != nil { + return false, err + } + + // Check that the major version number is supported. + if (vMajor < supported.MinMajor) || (vMajor > supported.MaxMajor) { + return false, nil + } + + // Check that the minor version number is supported + if (vMinor <= supported.MaxMinor) && (vMinor >= supported.MinMinor) { + return true, nil + } + + return false, nil +} + +// ParseVersion parsed the version strings v{MAJOR} and v{MAJOR}.{MINOR} into separate integers +// major and minor. +// For example, "v2.1" becomes 2 and 1, "v3" becomes 3 and 0, and "1" becomes 1 and 0. +func ParseVersion(version string) (major, minor int, err error) { + if version == "" { + return 0, 0, fmt.Errorf("empty version provided") + } + + // We use the regex indicated by the version discovery guidelines. + // + // https://specs.openstack.org/openstack/api-sig/guidelines/consuming-catalog/version-discovery.html#inferring-version + // + // However, we diverge slightly since not all services include the 'v' prefix (glares at zaqar) + versionRe := regexp.MustCompile(`^v?(?P[0-9]+)(\.(?P[0-9]+))?$`) + + match := versionRe.FindStringSubmatch(version) + if len(match) == 0 { + return 0, 0, fmt.Errorf("invalid format: %q", version) + } + + major, err = strconv.Atoi(match[versionRe.SubexpIndex("major")]) + if err != nil { + return 0, 0, err + } + + minor = 0 + if match[versionRe.SubexpIndex("minor")] != "" { + minor, err = strconv.Atoi(match[versionRe.SubexpIndex("minor")]) + if err != nil { + return 0, 0, err + } + } + + return major, minor, nil +} + +// ParseMicroversion parses the version major.minor into separate integers major and minor. +// For example, "2.53" becomes 2 and 53. +func ParseMicroversion(version string) (major int, minor int, err error) { + parts := strings.Split(version, ".") + if len(parts) != 2 { + return 0, 0, fmt.Errorf("invalid microversion format: %q", version) + } + major, err = strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, err + } + minor, err = strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, err + } + return major, minor, nil +} + +func ParseStatus(status string) (Status, error) { + switch strings.ToUpper(status) { + case "CURRENT", "STABLE": // keystone uses STABLE instead of CURRENT + return StatusCurrent, nil + case "SUPPORTED": + return StatusSupported, nil + case "DEPRECATED": + return StatusDeprecated, nil + case "": + return StatusUnknown, nil + default: + return StatusUnknown, fmt.Errorf("invalid status: %q", status) + } +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/pagination/http.go b/vendor/github.com/gophercloud/gophercloud/v2/pagination/http.go new file mode 100644 index 000000000..cf188b89b --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/pagination/http.go @@ -0,0 +1,63 @@ +package pagination + +import ( + "context" + "encoding/json" + "io" + "net/http" + "net/url" + "strings" + + "github.com/gophercloud/gophercloud/v2" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + gophercloud.Result + url.URL +} + +// PageResultFrom parses an HTTP response as JSON and returns a PageResult containing the +// results, interpreting it as JSON if the content type indicates. +func PageResultFrom(resp *http.Response) (PageResult, error) { + var parsedBody any + + defer resp.Body.Close() + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { + err = json.Unmarshal(rawBody, &parsedBody) + if err != nil { + return PageResult{}, err + } + } else { + parsedBody = rawBody + } + + return PageResultFromParsed(resp, parsedBody), err +} + +// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its +// body parsed as JSON (and closed). +func PageResultFromParsed(resp *http.Response, body any) PageResult { + return PageResult{ + Result: gophercloud.Result{ + Body: body, + StatusCode: resp.StatusCode, + Header: resp.Header, + }, + URL: *resp.Request.URL, + } +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(ctx context.Context, client *gophercloud.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(ctx, url, nil, &gophercloud.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204, 300}, + KeepResponseBody: true, + }) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/pagination/linked.go b/vendor/github.com/gophercloud/gophercloud/v2/pagination/linked.go new file mode 100644 index 000000000..7e4de4f7a --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/pagination/linked.go @@ -0,0 +1,92 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud/v2" +) + +// LinkedPageBase may be embedded to implement a page that provides navigational "Next" and "Previous" links within its result. +type LinkedPageBase struct { + PageResult + + // LinkPath lists the keys that should be traversed within a response to arrive at the "next" pointer. + // If any link along the path is missing, an empty URL will be returned. + // If any link results in an unexpected value type, an error will be returned. + // When left as "nil", []string{"links", "next"} will be used as a default. + LinkPath []string +} + +// NextPageURL extracts the pagination structure from a JSON response and returns the "next" link, if one is present. +// It assumes that the links are available in a "links" element of the top-level response object. +// If this is not the case, override NextPageURL on your result type. +func (current LinkedPageBase) NextPageURL() (string, error) { + var path []string + var key string + + if current.LinkPath == nil { + path = []string{"links", "next"} + } else { + path = current.LinkPath + } + + submap, ok := current.Body.(map[string]any) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]any" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return "", err + } + + for { + key, path = path[0], path[1:] + + value, ok := submap[key] + if !ok { + return "", nil + } + + if len(path) > 0 { + submap, ok = value.(map[string]any) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]any" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + } else { + if value == nil { + // Actual null element. + return "", nil + } + + url, ok := value.(string) + if !ok { + err := gophercloud.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]any); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]any" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current LinkedPageBase) GetBody() any { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/pagination/marker.go b/vendor/github.com/gophercloud/gophercloud/v2/pagination/marker.go new file mode 100644 index 000000000..1d101fe2d --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/pagination/marker.go @@ -0,0 +1,58 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud/v2" +) + +// MarkerPage is a stricter Page interface that describes additional functionality required for use with NewMarkerPager. +// For convenience, embed the MarkedPageBase struct. +type MarkerPage interface { + Page + + // LastMarker returns the last "marker" value on this page. + LastMarker() (string, error) +} + +// MarkerPageBase is a page in a collection that's paginated by "limit" and "marker" query parameters. +type MarkerPageBase struct { + PageResult + + // Owner is a reference to the embedding struct. + Owner MarkerPage +} + +// NextPageURL generates the URL for the page of results after this one. +func (current MarkerPageBase) NextPageURL() (string, error) { + currentURL := current.URL + + mark, err := current.Owner.LastMarker() + if err != nil { + return "", err + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current MarkerPageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]any); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]any" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current MarkerPageBase) GetBody() any { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/pagination/pager.go b/vendor/github.com/gophercloud/gophercloud/v2/pagination/pager.go new file mode 100644 index 000000000..358101256 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/pagination/pager.go @@ -0,0 +1,256 @@ +package pagination + +import ( + "context" + "errors" + "fmt" + "net/http" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud/v2" +) + +var ( + // ErrPageNotAvailable is returned from a Pager when a next or previous page is requested, but does not exist. + ErrPageNotAvailable = errors.New("The requested page does not exist.") +) + +// Page must be satisfied by the result type of any resource collection. +// It allows clients to interact with the resource uniformly, regardless of whether or not or how it's paginated. +// Generally, rather than implementing this interface directly, implementors should embed one of the concrete PageBase structs, +// instead. +// Depending on the pagination strategy of a particular resource, there may be an additional subinterface that the result type +// will need to implement. +type Page interface { + // NextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NextPageURL() (string, error) + + // IsEmpty returns true if this Page has no items in it. + IsEmpty() (bool, error) + + // GetBody returns the Page Body. This is used in the `AllPages` method. + GetBody() any +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *gophercloud.ServiceClient + + initialURL string + + createPage func(r PageResult) Page + + firstPage Page + + Err error + + // Headers supplies additional HTTP headers to populate on each paged request. + Headers map[string]string +} + +// NewPager constructs a manually-configured pager. +// Supply the URL for the first page, a function that requests a specific page given a URL, and a function that counts a page. +func NewPager(client *gophercloud.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +// WithPageCreator returns a new Pager that substitutes a different page creation function. This is +// useful for overriding List functions in delegation. +func (p Pager) WithPageCreator(createPage func(r PageResult) Page) Pager { + return Pager{ + client: p.client, + initialURL: p.initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(ctx context.Context, url string) (Page, error) { + resp, err := Request(ctx, p.client, p.Headers, url) + if err != nil { + return nil, err + } + + remembered, err := PageResultFrom(resp) + if err != nil { + return nil, err + } + + return p.createPage(remembered), nil +} + +// EachPage iterates over each page returned by a Pager, yielding one at a time +// to a handler function. Return "false" from the handler to prematurely stop +// iterating. +func (p Pager) EachPage(ctx context.Context, handler func(context.Context, Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + var currentPage Page + + // if first page has already been fetched, no need to fetch it again + if p.firstPage != nil { + currentPage = p.firstPage + p.firstPage = nil + } else { + var err error + currentPage, err = p.fetchNextPage(ctx, currentURL) + if err != nil { + return err + } + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(ctx, currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} + +// AllPages returns all the pages from a `List` operation in a single page, +// allowing the user to retrieve all the pages at once. +func (p Pager) AllPages(ctx context.Context) (Page, error) { + if p.Err != nil { + return nil, p.Err + } + // pagesSlice holds all the pages until they get converted into as Page Body. + var pagesSlice []any + // body will contain the final concatenated Page body. + var body reflect.Value + + // Grab a first page to ascertain the page body type. + firstPage, err := p.fetchNextPage(ctx, p.initialURL) + if err != nil { + return nil, err + } + // Store the page type so we can use reflection to create a new mega-page of + // that type. + pageType := reflect.TypeOf(firstPage) + + // if it's a single page, just return the firstPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return firstPage, nil + } + + // store the first page to avoid getting it twice + p.firstPage = firstPage + + // Switch on the page body type. Recognized types are `map[string]any`, + // `[]byte`, and `[]any`. + switch pb := firstPage.GetBody().(type) { + case map[string]any: + // key is the map key for the page body if the body type is `map[string]any`. + var key string + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(ctx, func(_ context.Context, page Page) (bool, error) { + b := page.GetBody().(map[string]any) + for k, v := range b { + // If it's a linked page, we don't want the `links`, we want the other one. + if !strings.HasSuffix(k, "links") { + // check the field's type. we only want []any (which is really []map[string]any) + switch vt := v.(type) { + case []any: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `map[string]any` + body = reflect.MakeMap(reflect.MapOf(reflect.TypeOf(key), reflect.TypeOf(pagesSlice))) + body.SetMapIndex(reflect.ValueOf(key), reflect.ValueOf(pagesSlice)) + case []byte: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(ctx, func(_ context.Context, page Page) (bool, error) { + b := page.GetBody().([]byte) + pagesSlice = append(pagesSlice, b) + // seperate pages with a comma + pagesSlice = append(pagesSlice, []byte{10}) + return true, nil + }) + if err != nil { + return nil, err + } + if len(pagesSlice) > 0 { + // Remove the trailing comma. + pagesSlice = pagesSlice[:len(pagesSlice)-1] + } + var b []byte + // Combine the slice of slices in to a single slice. + for _, slice := range pagesSlice { + b = append(b, slice.([]byte)...) + } + // Set body to value of type `bytes`. + body = reflect.New(reflect.TypeOf(b)).Elem() + body.SetBytes(b) + case []any: + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(ctx, func(_ context.Context, page Page) (bool, error) { + b := page.GetBody().([]any) + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + // Set body to value of type `[]any` + body = reflect.MakeSlice(reflect.TypeOf(pagesSlice), len(pagesSlice), len(pagesSlice)) + for i, s := range pagesSlice { + body.Index(i).Set(reflect.ValueOf(s)) + } + default: + err := gophercloud.ErrUnexpectedType{} + err.Expected = "map[string]any/[]byte/[]any" + err.Actual = fmt.Sprintf("%T", pb) + return nil, err + } + + // Each `Extract*` function is expecting a specific type of page coming back, + // otherwise the type assertion in those functions will fail. pageType is needed + // to create a type in this method that has the same type that the `Extract*` + // function is expecting and set the Body of that object to the concatenated + // pages. + page := reflect.New(pageType) + // Set the page body to be the concatenated pages. + page.Elem().FieldByName("Body").Set(body) + // Set any additional headers that were pass along. The `objectstorage` pacakge, + // for example, passes a Content-Type header. + h := make(http.Header) + for k, v := range p.Headers { + h.Add(k, v) + } + page.Elem().FieldByName("Header").Set(reflect.ValueOf(h)) + // Type assert the page to a Page interface so that the type assertion in the + // `Extract*` methods will work. + return page.Elem().Interface().(Page), err +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/pagination/pkg.go b/vendor/github.com/gophercloud/gophercloud/v2/pagination/pkg.go new file mode 100644 index 000000000..912daea36 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/pagination/pkg.go @@ -0,0 +1,4 @@ +/* +Package pagination contains utilities and convenience structs that implement common pagination idioms within OpenStack APIs. +*/ +package pagination diff --git a/vendor/github.com/gophercloud/gophercloud/v2/pagination/single.go b/vendor/github.com/gophercloud/gophercloud/v2/pagination/single.go new file mode 100644 index 000000000..416621121 --- /dev/null +++ b/vendor/github.com/gophercloud/gophercloud/v2/pagination/single.go @@ -0,0 +1,33 @@ +package pagination + +import ( + "fmt" + "reflect" + + "github.com/gophercloud/gophercloud/v2" +) + +// SinglePageBase may be embedded in a Page that contains all of the results from an operation at once. +type SinglePageBase PageResult + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty satisifies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + if b, ok := current.Body.([]any); ok { + return len(b) == 0, nil + } + err := gophercloud.ErrUnexpectedType{} + err.Expected = "[]any" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(current.Body)) + return true, err +} + +// GetBody returns the single page's body. This method is needed to satisfy the +// Page interface. +func (current SinglePageBase) GetBody() any { + return current.Body +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go b/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go index 52fcd38ab..9048e83de 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go @@ -7,13 +7,14 @@ import ( "errors" "io" "net/http" + "slices" "strings" "sync" ) // DefaultUserAgent is the default User-Agent string set in the request header. const ( - DefaultUserAgent = "gophercloud/v2.7.0" + DefaultUserAgent = "gophercloud/v2.9.0" DefaultMaxBackoffRetries = 60 ) @@ -437,16 +438,8 @@ func (client *ProviderClient) doRequest(ctx context.Context, method, url string, okc = defaultOkCodes(method) } - // Validate the HTTP response status. - var ok bool - for _, code := range okc { - if resp.StatusCode == code { - ok = true - break - } - } - - if !ok { + // Check the response code against the acceptable codes + if !slices.Contains(okc, resp.StatusCode) { body, _ := io.ReadAll(resp.Body) resp.Body.Close() respErr := ErrUnexpectedResponseCode{ diff --git a/vendor/github.com/gophercloud/gophercloud/v2/service_client.go b/vendor/github.com/gophercloud/gophercloud/v2/service_client.go index c1f9f41d4..015c3f233 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/service_client.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/service_client.go @@ -130,6 +130,9 @@ func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { opts.MoreHeaders["X-OpenStack-Ironic-API-Version"] = client.Microversion case "baremetal-introspection": opts.MoreHeaders["X-OpenStack-Ironic-Inspector-API-Version"] = client.Microversion + case "container-infrastructure-management", "container-infrastructure", "container-infra": + // magnum should accept container-infrastructure-management but (as of Epoxy) does not + serviceType = "container-infra" } if client.Type != "" { diff --git a/vendor/github.com/gophercloud/gophercloud/v2/util.go b/vendor/github.com/gophercloud/gophercloud/v2/util.go index ad8a7dfaa..d11a723b1 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/util.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/util.go @@ -37,9 +37,6 @@ func NormalizePathURL(basePath, rawPath string) (string, error) { absPathSys = filepath.Join(basePath, rawPath) u.Path = filepath.ToSlash(absPathSys) - if err != nil { - return "", err - } u.Scheme = "file" return u.String(), nil } diff --git a/vendor/github.com/gophercloud/utils/v2/LICENSE b/vendor/github.com/gophercloud/utils/v2/LICENSE new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/gophercloud/utils/v2/client/client.go b/vendor/github.com/gophercloud/utils/v2/client/client.go new file mode 100644 index 000000000..e2cd4ce47 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/client/client.go @@ -0,0 +1,404 @@ +package client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "sort" + "strconv" + "strings" + "time" + + "github.com/gophercloud/gophercloud/v2" +) + +// Logger is an interface representing the Logger struct +type Logger interface { + Printf(format string, args ...interface{}) +} + +// DefaultLogger is a default struct, which satisfies the Logger interface +type DefaultLogger struct{} + +// Printf is a default Printf method +func (DefaultLogger) Printf(format string, args ...interface{}) { + log.Printf("[DEBUG] "+format, args...) +} + +// noopLogger is a default noop logger satisfies the Logger interface +type noopLogger struct{} + +// Printf is a default noop method +func (noopLogger) Printf(format string, args ...interface{}) {} + +// RoundTripper satisfies the http.RoundTripper interface and is used to +// customize the default http client RoundTripper +type RoundTripper struct { + // Default http.RoundTripper + Rt http.RoundTripper + // Additional request headers to be set (not appended) in all client + // requests + headers *http.Header + // A pointer to a map of headers to be masked in logger + maskHeaders *map[string]struct{} + // A custom function to format and mask JSON requests and responses + FormatJSON func([]byte) (string, error) + // How many times HTTP connection should be retried until giving up + MaxRetries int + // If Logger is not nil, then RoundTrip method will debug the JSON + // requests and responses + Logger Logger +} + +// List of headers that contain sensitive data. +var defaultSensitiveHeaders = map[string]struct{}{ + "x-auth-token": {}, + "x-auth-key": {}, + "x-service-token": {}, + "x-storage-token": {}, + "x-account-meta-temp-url-key": {}, + "x-account-meta-temp-url-key-2": {}, + "x-container-meta-temp-url-key": {}, + "x-container-meta-temp-url-key-2": {}, + "set-cookie": {}, + "x-subject-token": {}, + "authorization": {}, +} + +// GetDefaultSensitiveHeaders returns the default list of headers to be masked +func GetDefaultSensitiveHeaders() []string { + headers := make([]string, len(defaultSensitiveHeaders)) + i := 0 + for k := range defaultSensitiveHeaders { + headers[i] = k + i++ + } + + return headers +} + +// SetSensitiveHeaders sets the list of case insensitive headers to be masked in +// debug log +func (rt *RoundTripper) SetSensitiveHeaders(headers []string) { + newHeaders := make(map[string]struct{}, len(headers)) + + for _, h := range headers { + newHeaders[h] = struct{}{} + } + + // this is concurrency safe + rt.maskHeaders = &newHeaders +} + +// SetHeaders sets request headers to be set (not appended) in all client +// requests +func (rt *RoundTripper) SetHeaders(headers http.Header) { + newHeaders := make(http.Header, len(headers)) + for k, v := range headers { + s := make([]string, len(v)) + for i, v := range v { + s[i] = v + } + newHeaders[k] = s + } + + // this is concurrency safe + rt.headers = &newHeaders +} + +func (rt *RoundTripper) hideSensitiveHeadersData(headers http.Header) []string { + result := make([]string, len(headers)) + headerIdx := 0 + + // this is concurrency safe + v := rt.maskHeaders + if v == nil { + v = &defaultSensitiveHeaders + } + maskHeaders := *v + + for header, data := range headers { + v := strings.ToLower(header) + if _, ok := maskHeaders[v]; ok { + result[headerIdx] = fmt.Sprintf("%s: %s", header, "***") + } else { + result[headerIdx] = fmt.Sprintf("%s: %s", header, strings.Join(data, " ")) + } + headerIdx++ + } + + return result +} + +// formatHeaders converts standard http.Header type to a string with separated headers. +// It will hide data of sensitive headers. +func (rt *RoundTripper) formatHeaders(headers http.Header, separator string) string { + redactedHeaders := rt.hideSensitiveHeadersData(headers) + sort.Strings(redactedHeaders) + + return strings.Join(redactedHeaders, separator) +} + +// RoundTrip performs a round-trip HTTP request and logs relevant information about it. +func (rt *RoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + defer func() { + if request.Body != nil { + request.Body.Close() + } + }() + + // for future reference, this is how to access the Transport struct: + //tlsconfig := rt.Rt.(*http.Transport).TLSClientConfig + + // this is concurrency safe + h := rt.headers + if h != nil { + for k, v := range *h { + // Set additional request headers + request.Header[k] = v + } + } + + var err error + + if rt.Logger != nil { + rt.log().Printf("OpenStack Request URL: %s %s", request.Method, request.URL) + rt.log().Printf("OpenStack Request Headers:\n%s", rt.formatHeaders(request.Header, "\n")) + + if request.Body != nil { + request.Body, err = rt.logRequest(request.Body, request.Header.Get("Content-Type")) + if err != nil { + return nil, err + } + } + } + + // this is concurrency safe + ort := rt.Rt + if ort == nil { + return nil, fmt.Errorf("Rt RoundTripper is nil, aborting") + } + response, err := ort.RoundTrip(request) + + // If the first request didn't return a response, retry up to `max_retries`. + retry := 1 + for response == nil { + if retry > rt.MaxRetries { + if rt.Logger != nil { + rt.log().Printf("OpenStack connection error, retries exhausted. Aborting") + } + err = fmt.Errorf("OpenStack connection error, retries exhausted. Aborting. Last error was: %s", err) + return nil, err + } + + if rt.Logger != nil { + rt.log().Printf("OpenStack connection error, retry number %d: %s", retry, err) + } + response, err = ort.RoundTrip(request) + retry += 1 + } + + if rt.Logger != nil { + rt.log().Printf("OpenStack Response Code: %d", response.StatusCode) + rt.log().Printf("OpenStack Response Headers:\n%s", rt.formatHeaders(response.Header, "\n")) + + response.Body, err = rt.logResponse(response.Body, response.Header.Get("Content-Type")) + } + + return response, err +} + +// logRequest will log the HTTP Request details. +// If the body is JSON, it will attempt to be pretty-formatted. +func (rt *RoundTripper) logRequest(original io.ReadCloser, contentType string) (io.ReadCloser, error) { + // Handle request contentType + if strings.HasPrefix(contentType, "application/json") || (strings.HasPrefix(contentType, "application/") && strings.HasSuffix(contentType, "-json-patch")) { + var bs bytes.Buffer + defer original.Close() + + _, err := io.Copy(&bs, original) + if err != nil { + return nil, err + } + + debugInfo, err := rt.formatJSON()(bs.Bytes()) + if err != nil { + rt.log().Printf("%s", err) + } + rt.log().Printf("OpenStack Request Body: %s", debugInfo) + + return ioutil.NopCloser(strings.NewReader(bs.String())), nil + } + + rt.log().Printf("Not logging because OpenStack request body isn't JSON") + return original, nil +} + +// logResponse will log the HTTP Response details. +// If the body is JSON, it will attempt to be pretty-formatted. +func (rt *RoundTripper) logResponse(original io.ReadCloser, contentType string) (io.ReadCloser, error) { + if strings.HasPrefix(contentType, "application/json") { + var bs bytes.Buffer + defer original.Close() + + _, err := io.Copy(&bs, original) + if err != nil { + return nil, err + } + + debugInfo, err := rt.formatJSON()(bs.Bytes()) + if err != nil { + rt.log().Printf("%s", err) + } + if debugInfo != "" { + rt.log().Printf("OpenStack Response Body: %s", debugInfo) + } + + return ioutil.NopCloser(strings.NewReader(bs.String())), nil + } + + rt.log().Printf("Not logging because OpenStack response body isn't JSON") + return original, nil +} + +func (rt *RoundTripper) formatJSON() func([]byte) (string, error) { + // this is concurrency safe + f := rt.FormatJSON + if f == nil { + return FormatJSON + } + return f +} + +func (rt *RoundTripper) log() Logger { + // this is concurrency safe + l := rt.Logger + if l == nil { + // noop is used, when logger pointer has been set to nil + return &noopLogger{} + } + return l +} + +// FormatJSON is a default function to pretty-format a JSON body. +// It will also mask known fields which contain sensitive information. +func FormatJSON(raw []byte) (string, error) { + var rawData interface{} + + err := json.Unmarshal(raw, &rawData) + if err != nil { + return string(raw), fmt.Errorf("unable to parse OpenStack JSON: %s", err) + } + + data, ok := rawData.(map[string]interface{}) + if !ok { + pretty, err := json.MarshalIndent(rawData, "", " ") + if err != nil { + return string(raw), fmt.Errorf("unable to re-marshal OpenStack JSON: %s", err) + } + + return string(pretty), nil + } + + // Mask known password fields + if v, ok := data["auth"].(map[string]interface{}); ok { + // v2 auth methods + if v, ok := v["passwordCredentials"].(map[string]interface{}); ok { + v["password"] = "***" + } + if v, ok := v["token"].(map[string]interface{}); ok { + v["id"] = "***" + } + // v3 auth methods + if v, ok := v["identity"].(map[string]interface{}); ok { + if v, ok := v["password"].(map[string]interface{}); ok { + if v, ok := v["user"].(map[string]interface{}); ok { + v["password"] = "***" + } + } + if v, ok := v["application_credential"].(map[string]interface{}); ok { + v["secret"] = "***" + } + if v, ok := v["token"].(map[string]interface{}); ok { + v["id"] = "***" + } + } + } + + // Mask EC2 access id and body hash + if v, ok := data["credentials"].(map[string]interface{}); ok { + var access string + if s, ok := v["access"]; ok { + access, _ = s.(string) + v["access"] = "***" + } + if _, ok := v["body_hash"]; ok { + v["body_hash"] = "***" + } + if v, ok := v["headers"].(map[string]interface{}); ok { + if _, ok := v["Authorization"]; ok { + if s, ok := v["Authorization"].(string); ok { + v["Authorization"] = strings.Replace(s, access, "***", -1) + } + } + } + } + + // Ignore the huge catalog output + if v, ok := data["token"].(map[string]interface{}); ok { + if _, ok := v["catalog"]; ok { + v["catalog"] = "***" + } + } + + pretty, err := json.MarshalIndent(data, "", " ") + if err != nil { + return string(raw), fmt.Errorf("unable to re-marshal OpenStack JSON: %s", err) + } + + return string(pretty), nil +} + +func RetryBackoffFunc(logger Logger) gophercloud.RetryBackoffFunc { + return func(ctx context.Context, respErr *gophercloud.ErrUnexpectedResponseCode, e error, retries uint) error { + retryAfter := respErr.ResponseHeader.Get("Retry-After") + if retryAfter == "" { + return e + } + + var sleep time.Duration + + // Parse delay seconds or HTTP date + if v, err := strconv.ParseUint(retryAfter, 10, 32); err == nil { + sleep = time.Duration(v) * time.Second + } else if v, err := time.Parse(http.TimeFormat, retryAfter); err == nil { + sleep = time.Until(v) + } else { + return e + } + + l := logger + if l != nil { + l.Printf("Received StatusTooManyRequests response code sleeping for %s", sleep) + } + if c := ctx; c != nil { + select { + case <-time.After(sleep): + case <-c.Done(): + if l != nil { + l.Printf("Sleeping aborted: %w", c.Err()) + } + return e + } + } else { + time.Sleep(sleep) + } + + return nil + } +} diff --git a/vendor/github.com/gophercloud/utils/v2/client/doc.go b/vendor/github.com/gophercloud/utils/v2/client/doc.go new file mode 100644 index 000000000..731530087 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/client/doc.go @@ -0,0 +1,146 @@ +/* +Package client provides an ability to create a http.RoundTripper OpenStack +client with extended options, including the JSON requests and responses log +capabilities. + +Example usage with the default logger: + + package example + + import ( + "net/http" + "os" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/utils/v2/client" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + ) + + func NewComputeV2Client() (*gophercloud.ServiceClient, error) { + ao, err := clientconfig.AuthOptions(nil) + if err != nil { + return nil, err + } + + provider, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return nil, err + } + + if os.Getenv("OS_DEBUG") != "" { + provider.HTTPClient = http.Client{ + Transport: &client.RoundTripper{ + Rt: &http.Transport{}, + Logger: &client.DefaultLogger{}, + }, + } + } + + err = openstack.Authenticate(provider, *ao) + if err != nil { + return nil, err + } + + return openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + } + +Example usage with the custom logger: + + package example + + import ( + "net/http" + "os" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/utils/v2/client" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + log "github.com/sirupsen/logrus" + ) + + type myLogger struct { + Prefix string + } + + func (l myLogger) Printf(format string, args ...interface{}) { + log.Debugf("%s [DEBUG] "+format, append([]interface{}{l.Prefix}, args...)...) + } + + func NewComputeV2Client() (*gophercloud.ServiceClient, error) { + ao, err := clientconfig.AuthOptions(nil) + if err != nil { + return nil, err + } + + provider, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return nil, err + } + + if os.Getenv("OS_DEBUG") != "" { + provider.HTTPClient = http.Client{ + Transport: &client.RoundTripper{ + Rt: &http.Transport{}, + Logger: &myLogger{Prefix: "myApp"}, + }, + } + } + + err = openstack.Authenticate(provider, *ao) + if err != nil { + return nil, err + } + + return openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + } + +Example usage with additinal headers: + + package example + + import ( + "net/http" + "os" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/utils/v2/client" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + ) + + func NewComputeV2Client() (*gophercloud.ServiceClient, error) { + ao, err := clientconfig.AuthOptions(nil) + if err != nil { + return nil, err + } + + provider, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return nil, err + } + + provider.HTTPClient = http.Client{ + Transport: &client.RoundTripper{ + Rt: &http.Transport{}, + }, + } + + provider.HTTPClient.Transport.(*client.RoundTripper).SetHeaders(map[string][]string{"Cache-Control": {"no-cache"}}}) + + err = openstack.Authenticate(provider, *ao) + if err != nil { + return nil, err + } + + return openstack.NewComputeV2(provider, gophercloud.EndpointOpts{ + Region: os.Getenv("OS_REGION_NAME"), + }) + } +*/ +package client diff --git a/vendor/github.com/gophercloud/utils/v2/env/env.go b/vendor/github.com/gophercloud/utils/v2/env/env.go new file mode 100644 index 000000000..23d4516a7 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/env/env.go @@ -0,0 +1,12 @@ +//go:build !windows +// +build !windows + +package env + +import ( + "os" +) + +func Getenv(s string) string { + return os.Getenv(s) +} diff --git a/vendor/github.com/gophercloud/utils/v2/env/env_windows.go b/vendor/github.com/gophercloud/utils/v2/env/env_windows.go new file mode 100644 index 000000000..7cf80ca1d --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/env/env_windows.go @@ -0,0 +1,106 @@ +package env + +import ( + "os" + "syscall" + + "golang.org/x/sys/windows" + "golang.org/x/text/encoding/charmap" +) + +func Getenv(s string) string { + var st uint32 + env := os.Getenv(s) + if windows.GetConsoleMode(windows.Handle(syscall.Stdin), &st) == nil || + windows.GetConsoleMode(windows.Handle(syscall.Stdout), &st) == nil || + windows.GetConsoleMode(windows.Handle(syscall.Stderr), &st) == nil { + // detect windows console, should be skipped in cygwin environment + var cm charmap.Charmap + switch windows.GetACP() { + case 37: + cm = *charmap.CodePage037 + case 1047: + cm = *charmap.CodePage1047 + case 1140: + cm = *charmap.CodePage1140 + case 437: + cm = *charmap.CodePage437 + case 850: + cm = *charmap.CodePage850 + case 852: + cm = *charmap.CodePage852 + case 855: + cm = *charmap.CodePage855 + case 858: + cm = *charmap.CodePage858 + case 860: + cm = *charmap.CodePage860 + case 862: + cm = *charmap.CodePage862 + case 863: + cm = *charmap.CodePage863 + case 865: + cm = *charmap.CodePage865 + case 866: + cm = *charmap.CodePage866 + case 28591: + cm = *charmap.ISO8859_1 + case 28592: + cm = *charmap.ISO8859_2 + case 28593: + cm = *charmap.ISO8859_3 + case 28594: + cm = *charmap.ISO8859_4 + case 28595: + cm = *charmap.ISO8859_5 + case 28596: + cm = *charmap.ISO8859_6 + case 28597: + cm = *charmap.ISO8859_7 + case 28598: + cm = *charmap.ISO8859_8 + case 28599: + cm = *charmap.ISO8859_9 + case 28600: + cm = *charmap.ISO8859_10 + case 28603: + cm = *charmap.ISO8859_13 + case 28604: + cm = *charmap.ISO8859_14 + case 28605: + cm = *charmap.ISO8859_15 + case 28606: + cm = *charmap.ISO8859_16 + case 20866: + cm = *charmap.KOI8R + case 21866: + cm = *charmap.KOI8U + case 1250: + cm = *charmap.Windows1250 + case 1251: + cm = *charmap.Windows1251 + case 1252: + cm = *charmap.Windows1252 + case 1253: + cm = *charmap.Windows1253 + case 1254: + cm = *charmap.Windows1254 + case 1255: + cm = *charmap.Windows1255 + case 1256: + cm = *charmap.Windows1256 + case 1257: + cm = *charmap.Windows1257 + case 1258: + cm = *charmap.Windows1258 + case 874: + cm = *charmap.Windows874 + default: + return env + } + if v, err := cm.NewEncoder().String(env); err == nil { + return v + } + } + return env +} diff --git a/vendor/github.com/gophercloud/utils/v2/gnocchi/client.go b/vendor/github.com/gophercloud/utils/v2/gnocchi/client.go new file mode 100644 index 000000000..a8cc0ded4 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/gnocchi/client.go @@ -0,0 +1,25 @@ +package gnocchi + +import ( + "github.com/gophercloud/gophercloud/v2" +) + +func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) { + sc := new(gophercloud.ServiceClient) + eo.ApplyDefaults(clientType) + url, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = url + sc.Type = clientType + return sc, nil +} + +// NewGnocchiV1 creates a ServiceClient that may be used with the v1 Gnocchi package. +func NewGnocchiV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "metric") + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} diff --git a/vendor/github.com/gophercloud/utils/v2/gnocchi/results.go b/vendor/github.com/gophercloud/utils/v2/gnocchi/results.go new file mode 100644 index 000000000..f08095d3f --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/gnocchi/results.go @@ -0,0 +1,37 @@ +package gnocchi + +import ( + "bytes" + "encoding/json" + "time" +) + +// RFC3339NanoTimezone describes a common timestamp format used by Gnocchi API responses. +const RFC3339NanoTimezone = "2006-01-02T15:04:05.999999+00:00" + +// RFC3339NanoNoTimezone describes a common timestamp format that can be used for Gnocchi requests +// with time ranges. +const RFC3339NanoNoTimezone = "2006-01-02T15:04:05.999999" + +// JSONRFC3339NanoTimezone is a type for Gnocchi responses timestamps with a timezone offset. +type JSONRFC3339NanoTimezone time.Time + +// UnmarshalJSON helps to unmarshal timestamps from Gnocchi responses to the +// JSONRFC3339NanoTimezone type. +func (jt *JSONRFC3339NanoTimezone) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NanoTimezone, s) + if err != nil { + return err + } + *jt = JSONRFC3339NanoTimezone(t) + return nil +} diff --git a/vendor/github.com/gophercloud/utils/v2/internal/pkg.go b/vendor/github.com/gophercloud/utils/v2/internal/pkg.go new file mode 100644 index 000000000..5bf0569ce --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/internal/pkg.go @@ -0,0 +1 @@ +package internal diff --git a/vendor/github.com/gophercloud/utils/v2/internal/util.go b/vendor/github.com/gophercloud/utils/v2/internal/util.go new file mode 100644 index 000000000..887bb8f3d --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/internal/util.go @@ -0,0 +1,111 @@ +package internal + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "os" + "reflect" + "strings" + + "github.com/mitchellh/go-homedir" +) + +// RemainingKeys will inspect a struct and compare it to a map. Any struct +// field that does not have a JSON tag that matches a key in the map or +// a matching lower-case field in the map will be returned as an extra. +// +// This is useful for determining the extra fields returned in response bodies +// for resources that can contain an arbitrary or dynamic number of fields. +func RemainingKeys(s interface{}, m map[string]interface{}) (extras map[string]interface{}) { + extras = make(map[string]interface{}) + for k, v := range m { + extras[k] = v + } + + valueOf := reflect.ValueOf(s) + typeOf := reflect.TypeOf(s) + for i := 0; i < valueOf.NumField(); i++ { + field := typeOf.Field(i) + + lowerField := strings.ToLower(field.Name) + delete(extras, lowerField) + + if tagValue := field.Tag.Get("json"); tagValue != "" && tagValue != "-" { + delete(extras, tagValue) + } + } + + return +} + +// PrepareTLSConfig generates TLS config based on the specifed parameters +func PrepareTLSConfig(caCertFile, clientCertFile, clientKeyFile string, insecure *bool) (*tls.Config, error) { + config := &tls.Config{} + if caCertFile != "" { + caCert, _, err := pathOrContents(caCertFile) + if err != nil { + return nil, fmt.Errorf("Error reading CA Cert: %s", err) + } + + caCertPool := x509.NewCertPool() + if ok := caCertPool.AppendCertsFromPEM(bytes.TrimSpace(caCert)); !ok { + return nil, fmt.Errorf("Error parsing CA Cert from %s", caCertFile) + } + config.RootCAs = caCertPool + } + + if insecure == nil { + config.InsecureSkipVerify = false + } else { + config.InsecureSkipVerify = *insecure + } + + if clientCertFile != "" && clientKeyFile != "" { + clientCert, _, err := pathOrContents(clientCertFile) + if err != nil { + return nil, fmt.Errorf("Error reading Client Cert: %s", err) + } + clientKey, _, err := pathOrContents(clientKeyFile) + if err != nil { + return nil, fmt.Errorf("Error reading Client Key: %s", err) + } + + cert, err := tls.X509KeyPair(clientCert, clientKey) + if err != nil { + return nil, err + } + + config.Certificates = []tls.Certificate{cert} + config.BuildNameToCertificate() + } + + return config, nil +} + +func pathOrContents(poc string) ([]byte, bool, error) { + if len(poc) == 0 { + return nil, false, nil + } + + path := poc + if path[0] == '~' { + var err error + path, err = homedir.Expand(path) + if err != nil { + return []byte(path), true, err + } + } + + if _, err := os.Stat(path); err == nil { + contents, err := ioutil.ReadFile(path) + if err != nil { + return contents, true, err + } + return contents, true, nil + } + + return []byte(poc), false, nil +} diff --git a/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/doc.go b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/doc.go new file mode 100644 index 000000000..3f85b6731 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/doc.go @@ -0,0 +1,46 @@ +/* +Package clientconfig provides convienent functions for creating OpenStack +clients. It is based on the Python os-client-config library. + +See https://docs.openstack.org/os-client-config/latest for details. + +Example to Create a Provider Client From clouds.yaml + + opts := &clientconfig.ClientOpts{ + Cloud: "hawaii", + } + + pClient, err := clientconfig.AuthenticatedClient(ctx, opts) + if err != nil { + panic(err) + } + +Example to Manually Create a Provider Client + + opts := &clientconfig.ClientOpts{ + AuthInfo: &clientconfig.AuthInfo{ + AuthURL: "https://hi.example.com:5000/v3", + Username: "jdoe", + Password: "password", + ProjectName: "Some Project", + DomainName: "default", + }, + } + + pClient, err := clientconfig.AuthenticatedClient(ctx, opts) + if err != nil { + panic(err) + } + +Example to Create a Service Client from clouds.yaml + + opts := &clientconfig.ClientOpts{ + Cloud: "hawaii", + } + + computeClient, err := clientconfig.NewServiceClient(ctx, "compute", opts) + if err != nil { + panic(err) + } +*/ +package clientconfig diff --git a/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/requests.go b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/requests.go new file mode 100644 index 000000000..f3347748e --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/requests.go @@ -0,0 +1,1023 @@ +package clientconfig + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "reflect" + "strings" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/utils/v2/env" + "github.com/gophercloud/utils/v2/gnocchi" + "github.com/gophercloud/utils/v2/internal" + + "github.com/gofrs/uuid/v5" + + yaml "gopkg.in/yaml.v3" +) + +// AuthType respresents a valid method of authentication. +type AuthType string + +const ( + // AuthPassword defines an unknown version of the password + AuthPassword AuthType = "password" + // AuthToken defined an unknown version of the token + AuthToken AuthType = "token" + + // AuthV2Password defines version 2 of the password + AuthV2Password AuthType = "v2password" + // AuthV2Token defines version 2 of the token + AuthV2Token AuthType = "v2token" + + // AuthV3Password defines version 3 of the password + AuthV3Password AuthType = "v3password" + // AuthV3Token defines version 3 of the token + AuthV3Token AuthType = "v3token" + + // AuthV3ApplicationCredential defines version 3 of the application credential + AuthV3ApplicationCredential AuthType = "v3applicationcredential" +) + +// ClientOpts represents options to customize the way a client is +// configured. +type ClientOpts struct { + // Cloud is the cloud entry in clouds.yaml to use. + Cloud string + + // EnvPrefix allows a custom environment variable prefix to be used. + EnvPrefix string + + // AuthType specifies the type of authentication to use. + // By default, this is "password". + AuthType AuthType + + // AuthInfo defines the authentication information needed to + // authenticate to a cloud when clouds.yaml isn't used. + AuthInfo *AuthInfo + + // RegionName is the region to create a Service Client in. + // This will override a region in clouds.yaml or can be used + // when authenticating directly with AuthInfo. + RegionName string + + // EndpointType specifies whether to use the public, internal, or + // admin endpoint of a service. + EndpointType string + + // HTTPClient provides the ability customize the ProviderClient's + // internal HTTP client. + HTTPClient *http.Client + + // YAMLOpts provides the ability to pass a customized set + // of options and methods for loading the YAML file. + // It takes a YAMLOptsBuilder interface that is defined + // in this file. This is optional and the default behavior + // is to call the local LoadCloudsYAML functions defined + // in this file. + YAMLOpts YAMLOptsBuilder +} + +// YAMLOptsBuilder defines an interface for customization when +// loading a clouds.yaml file. +type YAMLOptsBuilder interface { + LoadCloudsYAML() (map[string]Cloud, error) + LoadSecureCloudsYAML() (map[string]Cloud, error) + LoadPublicCloudsYAML() (map[string]Cloud, error) +} + +// YAMLOpts represents options and methods to load a clouds.yaml file. +type YAMLOpts struct { + // By default, no options are specified. +} + +// LoadCloudsYAML defines how to load a clouds.yaml file. +// By default, this calls the local LoadCloudsYAML function. +func (opts YAMLOpts) LoadCloudsYAML() (map[string]Cloud, error) { + return LoadCloudsYAML() +} + +// LoadSecureCloudsYAML defines how to load a secure.yaml file. +// By default, this calls the local LoadSecureCloudsYAML function. +func (opts YAMLOpts) LoadSecureCloudsYAML() (map[string]Cloud, error) { + return LoadSecureCloudsYAML() +} + +// LoadPublicCloudsYAML defines how to load a public-secure.yaml file. +// By default, this calls the local LoadPublicCloudsYAML function. +func (opts YAMLOpts) LoadPublicCloudsYAML() (map[string]Cloud, error) { + return LoadPublicCloudsYAML() +} + +// LoadCloudsYAML will load a clouds.yaml file and return the full config. +// This is called by the YAMLOpts method. Calling this function directly +// is supported for now but has only been retained for backwards +// compatibility from before YAMLOpts was defined. This may be removed in +// the future. +func LoadCloudsYAML() (map[string]Cloud, error) { + _, content, err := FindAndReadCloudsYAML() + if err != nil { + return nil, err + } + + var clouds Clouds + err = yaml.Unmarshal(content, &clouds) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %w", err) + } + + return clouds.Clouds, nil +} + +// LoadSecureCloudsYAML will load a secure.yaml file and return the full config. +// This is called by the YAMLOpts method. Calling this function directly +// is supported for now but has only been retained for backwards +// compatibility from before YAMLOpts was defined. This may be removed in +// the future. +func LoadSecureCloudsYAML() (map[string]Cloud, error) { + var secureClouds Clouds + + _, content, err := FindAndReadSecureCloudsYAML() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // secure.yaml is optional so just ignore read error + return secureClouds.Clouds, nil + } + return nil, err + } + + err = yaml.Unmarshal(content, &secureClouds) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %w", err) + } + + return secureClouds.Clouds, nil +} + +// LoadPublicCloudsYAML will load a public-clouds.yaml file and return the full config. +// This is called by the YAMLOpts method. Calling this function directly +// is supported for now but has only been retained for backwards +// compatibility from before YAMLOpts was defined. This may be removed in +// the future. +func LoadPublicCloudsYAML() (map[string]Cloud, error) { + var publicClouds PublicClouds + + _, content, err := FindAndReadPublicCloudsYAML() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + // clouds-public.yaml is optional so just ignore read error + return publicClouds.Clouds, nil + } + + return nil, err + } + + err = yaml.Unmarshal(content, &publicClouds) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal yaml: %w", err) + } + + return publicClouds.Clouds, nil +} + +// GetCloudFromYAML will return a cloud entry from a clouds.yaml file. +func GetCloudFromYAML(opts *ClientOpts) (*Cloud, error) { + if opts == nil { + opts = new(ClientOpts) + } + + if opts.YAMLOpts == nil { + opts.YAMLOpts = new(YAMLOpts) + } + + yamlOpts := opts.YAMLOpts + + clouds, err := yamlOpts.LoadCloudsYAML() + if err != nil { + return nil, fmt.Errorf("unable to load clouds.yaml: %w", err) + } + + // Determine which cloud to use. + // First see if a cloud name was explicitly set in opts. + var cloudName string + if opts.Cloud != "" { + cloudName = opts.Cloud + } else { + // If not, see if a cloud name was specified as an environment variable. + envPrefix := "OS_" + if opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "CLOUD"); v != "" { + cloudName = v + } + } + + var cloud *Cloud + if cloudName != "" { + v, ok := clouds[cloudName] + if !ok { + return nil, fmt.Errorf("cloud %s does not exist in clouds.yaml", cloudName) + } + cloud = &v + } + + // If a cloud was not specified, and clouds only contains + // a single entry, use that entry. + if cloudName == "" && len(clouds) == 1 { + for _, v := range clouds { + cloud = &v + } + } + + if cloud != nil { + // A profile points to a public cloud entry. + // If one was specified, load a list of public clouds + // and then merge the information with the current cloud data. + profileName := defaultIfEmpty(cloud.Profile, cloud.Cloud) + + if profileName != "" { + publicClouds, err := yamlOpts.LoadPublicCloudsYAML() + if err != nil { + return nil, fmt.Errorf("unable to load clouds-public.yaml: %w", err) + } + + publicCloud, ok := publicClouds[profileName] + if !ok { + return nil, fmt.Errorf("cloud %s does not exist in clouds-public.yaml", profileName) + } + + cloud, err = mergeClouds(cloud, publicCloud) + if err != nil { + return nil, fmt.Errorf("Could not merge information from clouds.yaml and clouds-public.yaml for cloud %s", profileName) + } + } + } + + // Next, load a secure clouds file and see if a cloud entry + // can be found or merged. + secureClouds, err := yamlOpts.LoadSecureCloudsYAML() + if err != nil { + return nil, fmt.Errorf("unable to load secure.yaml: %w", err) + } + + if secureClouds != nil { + // If no entry was found in clouds.yaml, no cloud name was specified, + // and only one secureCloud entry exists, use that as the cloud entry. + if cloud == nil && cloudName == "" && len(secureClouds) == 1 { + for _, v := range secureClouds { + cloud = &v + } + } + + // Otherwise, see if the provided cloud name exists in the secure yaml file. + secureCloud, ok := secureClouds[cloudName] + if !ok && cloud == nil { + // cloud == nil serves two purposes here: + // if no entry in clouds.yaml was found and + // if a single-entry secureCloud wasn't used. + // At this point, no entry could be determined at all. + return nil, fmt.Errorf("Could not find cloud %s", cloudName) + } + + // If secureCloud has content and it differs from the cloud entry, + // merge the two together. + if !reflect.DeepEqual((Cloud{}), secureCloud) && !reflect.DeepEqual(cloud, secureCloud) { + cloud, err = mergeClouds(secureCloud, cloud) + if err != nil { + return nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml") + } + } + } + + // As an extra precaution, do one final check to see if cloud is nil. + // We shouldn't reach this point, though. + if cloud == nil { + return nil, fmt.Errorf("Could not find cloud %s", cloudName) + } + + // Default is to verify SSL API requests + if cloud.Verify == nil { + iTrue := true + cloud.Verify = &iTrue + } + + // merging per-region value overrides + if opts.RegionName != "" { + for _, v := range cloud.Regions { + if opts.RegionName == v.Name { + cloud, err = mergeClouds(v.Values, cloud) + break + } + } + } + + // TODO: this is where reading vendor files should go be considered when not found in + // clouds-public.yml + // https://github.com/openstack/openstacksdk/tree/master/openstack/config/vendors + + // Both Interface and EndpointType are valid settings in clouds.yaml, + // but we want to standardize on EndpointType for simplicity. + // + // If only Interface was set, we copy that to EndpointType to use as the setting. + // But in all other cases, EndpointType is used and Interface is cleared. + if cloud.Interface != "" && cloud.EndpointType == "" { + cloud.EndpointType = cloud.Interface + } + + cloud.Interface = "" + + return cloud, err +} + +// AuthOptions creates a gophercloud.AuthOptions structure with the +// settings found in a specific cloud entry of a clouds.yaml file or +// based on authentication settings given in ClientOpts. +// +// This attempts to be a single point of entry for all OpenStack authentication. +// +// See http://docs.openstack.org/developer/os-client-config and +// https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py. +func AuthOptions(opts *ClientOpts) (*gophercloud.AuthOptions, error) { + cloud := new(Cloud) + + // If no opts were passed in, create an empty ClientOpts. + if opts == nil { + opts = new(ClientOpts) + } + + // Determine if a clouds.yaml entry should be retrieved. + // Start by figuring out the cloud name. + // First check if one was explicitly specified in opts. + var cloudName string + if opts.Cloud != "" { + cloudName = opts.Cloud + } else { + // If not, see if a cloud name was specified as an environment + // variable. + envPrefix := "OS_" + if opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "CLOUD"); v != "" { + cloudName = v + } + } + + // If a cloud name was determined, try to look it up in clouds.yaml. + if cloudName != "" { + // Get the requested cloud. + var err error + cloud, err = GetCloudFromYAML(opts) + if err != nil { + return nil, err + } + } + + // If cloud.AuthInfo is nil, then no cloud was specified. + if cloud.AuthInfo == nil { + // If opts.AuthInfo is not nil, then try using the auth settings from it. + if opts.AuthInfo != nil { + cloud.AuthInfo = opts.AuthInfo + } + + // If cloud.AuthInfo is still nil, then set it to an empty Auth struct + // and rely on environment variables to do the authentication. + if cloud.AuthInfo == nil { + cloud.AuthInfo = new(AuthInfo) + } + } + + identityAPI := determineIdentityAPI(cloud, opts) + switch identityAPI { + case "2.0", "2": + return v2auth(cloud, opts) + case "3": + return v3auth(cloud, opts) + } + + return nil, fmt.Errorf("Unable to build AuthOptions") +} + +func determineIdentityAPI(cloud *Cloud, opts *ClientOpts) string { + var identityAPI string + if cloud.IdentityAPIVersion != "" { + identityAPI = cloud.IdentityAPIVersion + } + + envPrefix := "OS_" + if opts != nil && opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "IDENTITY_API_VERSION"); v != "" { + identityAPI = v + } + + if identityAPI == "" { + if cloud.AuthInfo != nil { + if strings.Contains(cloud.AuthInfo.AuthURL, "v2.0") { + identityAPI = "2.0" + } + + if strings.Contains(cloud.AuthInfo.AuthURL, "v3") { + identityAPI = "3" + } + } + } + + if identityAPI == "" { + switch cloud.AuthType { + case AuthV2Password: + identityAPI = "2.0" + case AuthV2Token: + identityAPI = "2.0" + case AuthV3Password: + identityAPI = "3" + case AuthV3Token: + identityAPI = "3" + case AuthV3ApplicationCredential: + identityAPI = "3" + } + } + + // If an Identity API version could not be determined, + // default to v3. + if identityAPI == "" { + identityAPI = "3" + } + + return identityAPI +} + +// v2auth creates a v2-compatible gophercloud.AuthOptions struct. +func v2auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) { + // Environment variable overrides. + envPrefix := "OS_" + if opts != nil && opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if cloud.AuthInfo.AuthURL == "" { + if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" { + cloud.AuthInfo.AuthURL = v + } + } + + if cloud.AuthInfo.Token == "" { + if v := env.Getenv(envPrefix + "TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + + if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + } + + if cloud.AuthInfo.Username == "" { + if v := env.Getenv(envPrefix + "USERNAME"); v != "" { + cloud.AuthInfo.Username = v + } + } + + if cloud.AuthInfo.Password == "" { + if v := env.Getenv(envPrefix + "PASSWORD"); v != "" { + cloud.AuthInfo.Password = v + } + } + + if cloud.AuthInfo.ProjectID == "" { + if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + + if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + } + + if cloud.AuthInfo.ProjectName == "" { + if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + + if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + } + + ao := &gophercloud.AuthOptions{ + IdentityEndpoint: cloud.AuthInfo.AuthURL, + TokenID: cloud.AuthInfo.Token, + Username: cloud.AuthInfo.Username, + Password: cloud.AuthInfo.Password, + TenantID: cloud.AuthInfo.ProjectID, + TenantName: cloud.AuthInfo.ProjectName, + AllowReauth: cloud.AuthInfo.AllowReauth, + } + + return ao, nil +} + +// v3auth creates a v3-compatible gophercloud.AuthOptions struct. +func v3auth(cloud *Cloud, opts *ClientOpts) (*gophercloud.AuthOptions, error) { + // Environment variable overrides. + envPrefix := "OS_" + if opts != nil && opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if cloud.AuthInfo.AuthURL == "" { + if v := env.Getenv(envPrefix + "AUTH_URL"); v != "" { + cloud.AuthInfo.AuthURL = v + } + } + + if cloud.AuthInfo.Token == "" { + if v := env.Getenv(envPrefix + "TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + + if v := env.Getenv(envPrefix + "AUTH_TOKEN"); v != "" { + cloud.AuthInfo.Token = v + } + } + + if cloud.AuthInfo.Username == "" { + if v := env.Getenv(envPrefix + "USERNAME"); v != "" { + cloud.AuthInfo.Username = v + } + } + + if cloud.AuthInfo.UserID == "" { + if v := env.Getenv(envPrefix + "USER_ID"); v != "" { + cloud.AuthInfo.UserID = v + } + } + + if cloud.AuthInfo.Password == "" { + if v := env.Getenv(envPrefix + "PASSWORD"); v != "" { + cloud.AuthInfo.Password = v + } + } + + if cloud.AuthInfo.ProjectID == "" { + if v := env.Getenv(envPrefix + "TENANT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + + if v := env.Getenv(envPrefix + "PROJECT_ID"); v != "" { + cloud.AuthInfo.ProjectID = v + } + } + + if cloud.AuthInfo.ProjectName == "" { + if v := env.Getenv(envPrefix + "TENANT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + + if v := env.Getenv(envPrefix + "PROJECT_NAME"); v != "" { + cloud.AuthInfo.ProjectName = v + } + } + + if cloud.AuthInfo.DomainID == "" { + if v := env.Getenv(envPrefix + "DOMAIN_ID"); v != "" { + cloud.AuthInfo.DomainID = v + } + } + + if cloud.AuthInfo.DomainName == "" { + if v := env.Getenv(envPrefix + "DOMAIN_NAME"); v != "" { + cloud.AuthInfo.DomainName = v + } + } + + if cloud.AuthInfo.DefaultDomain == "" { + if v := env.Getenv(envPrefix + "DEFAULT_DOMAIN"); v != "" { + cloud.AuthInfo.DefaultDomain = v + } + } + + if cloud.AuthInfo.ProjectDomainID == "" { + if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_ID"); v != "" { + cloud.AuthInfo.ProjectDomainID = v + } + } + + if cloud.AuthInfo.ProjectDomainName == "" { + if v := env.Getenv(envPrefix + "PROJECT_DOMAIN_NAME"); v != "" { + cloud.AuthInfo.ProjectDomainName = v + } + } + + if cloud.AuthInfo.UserDomainID == "" { + if v := env.Getenv(envPrefix + "USER_DOMAIN_ID"); v != "" { + cloud.AuthInfo.UserDomainID = v + } + } + + if cloud.AuthInfo.UserDomainName == "" { + if v := env.Getenv(envPrefix + "USER_DOMAIN_NAME"); v != "" { + cloud.AuthInfo.UserDomainName = v + } + } + + if cloud.AuthInfo.ApplicationCredentialID == "" { + if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_ID"); v != "" { + cloud.AuthInfo.ApplicationCredentialID = v + } + } + + if cloud.AuthInfo.ApplicationCredentialName == "" { + if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_NAME"); v != "" { + cloud.AuthInfo.ApplicationCredentialName = v + } + } + + if cloud.AuthInfo.ApplicationCredentialSecret == "" { + if v := env.Getenv(envPrefix + "APPLICATION_CREDENTIAL_SECRET"); v != "" { + cloud.AuthInfo.ApplicationCredentialSecret = v + } + } + + if cloud.AuthInfo.SystemScope == "" { + if v := env.Getenv(envPrefix + "SYSTEM_SCOPE"); v != "" { + cloud.AuthInfo.SystemScope = v + } + } + + // Build a scope and try to do it correctly. + // https://github.com/openstack/os-client-config/blob/master/os_client_config/config.py#L595 + scope := new(gophercloud.AuthScope) + + // Application credentials don't support scope + if isApplicationCredential(cloud.AuthInfo) { + // If Domain* is set, but UserDomain* or ProjectDomain* aren't, + // then use Domain* as the default setting. + cloud = setDomainIfNeeded(cloud) + } else { + if !isProjectScoped(cloud.AuthInfo) { + if cloud.AuthInfo.DomainID != "" { + scope.DomainID = cloud.AuthInfo.DomainID + } else if cloud.AuthInfo.DomainName != "" { + scope.DomainName = cloud.AuthInfo.DomainName + } + if cloud.AuthInfo.SystemScope != "" { + scope.System = true + } + } else { + // If Domain* is set, but UserDomain* or ProjectDomain* aren't, + // then use Domain* as the default setting. + cloud = setDomainIfNeeded(cloud) + + if cloud.AuthInfo.ProjectID != "" { + scope.ProjectID = cloud.AuthInfo.ProjectID + } else { + scope.ProjectName = cloud.AuthInfo.ProjectName + scope.DomainID = cloud.AuthInfo.ProjectDomainID + scope.DomainName = cloud.AuthInfo.ProjectDomainName + } + } + } + + ao := &gophercloud.AuthOptions{ + Scope: scope, + IdentityEndpoint: cloud.AuthInfo.AuthURL, + TokenID: cloud.AuthInfo.Token, + Username: cloud.AuthInfo.Username, + UserID: cloud.AuthInfo.UserID, + Password: cloud.AuthInfo.Password, + TenantID: cloud.AuthInfo.ProjectID, + TenantName: cloud.AuthInfo.ProjectName, + DomainID: cloud.AuthInfo.UserDomainID, + DomainName: cloud.AuthInfo.UserDomainName, + ApplicationCredentialID: cloud.AuthInfo.ApplicationCredentialID, + ApplicationCredentialName: cloud.AuthInfo.ApplicationCredentialName, + ApplicationCredentialSecret: cloud.AuthInfo.ApplicationCredentialSecret, + AllowReauth: cloud.AuthInfo.AllowReauth, + } + + // If an auth_type of "token" was specified, then make sure + // Gophercloud properly authenticates with a token. This involves + // unsetting a few other auth options. The reason this is done + // here is to wait until all auth settings (both in clouds.yaml + // and via environment variables) are set and then unset them. + if strings.Contains(string(cloud.AuthType), "token") || ao.TokenID != "" { + ao.Username = "" + ao.Password = "" + ao.UserID = "" + ao.DomainID = "" + ao.DomainName = "" + } + + // Check for absolute minimum requirements. + if ao.IdentityEndpoint == "" { + err := gophercloud.ErrMissingInput{Argument: "auth_url"} + return nil, err + } + + return ao, nil +} + +// AuthenticatedClient is a convenience function to get a new provider client +// based on a clouds.yaml entry. +func AuthenticatedClient(ctx context.Context, opts *ClientOpts) (*gophercloud.ProviderClient, error) { + ao, err := AuthOptions(opts) + if err != nil { + return nil, err + } + + return openstack.AuthenticatedClient(ctx, *ao) +} + +// NewServiceClient is a convenience function to get a new service client. +func NewServiceClient(ctx context.Context, service string, opts *ClientOpts) (*gophercloud.ServiceClient, error) { + cloud := new(Cloud) + + // If no opts were passed in, create an empty ClientOpts. + if opts == nil { + opts = new(ClientOpts) + } + + // Determine if a clouds.yaml entry should be retrieved. + // Start by figuring out the cloud name. + // First check if one was explicitly specified in opts. + var cloudName string + if opts.Cloud != "" { + cloudName = opts.Cloud + } + + // Next see if a cloud name was specified as an environment variable. + envPrefix := "OS_" + if opts.EnvPrefix != "" { + envPrefix = opts.EnvPrefix + } + + if v := env.Getenv(envPrefix + "CLOUD"); v != "" { + cloudName = v + } + + // If a cloud name was determined, try to look it up in clouds.yaml. + if cloudName != "" { + // Get the requested cloud. + var err error + cloud, err = GetCloudFromYAML(opts) + if err != nil { + return nil, err + } + } + + // Check if a custom CA cert was provided. + // First, check if the CACERT environment variable is set. + var caCertPath string + if v := env.Getenv(envPrefix + "CACERT"); v != "" { + caCertPath = v + } + // Next, check if the cloud entry sets a CA cert. + if v := cloud.CACertFile; v != "" { + caCertPath = v + } + + // Check if a custom client cert was provided. + // First, check if the CERT environment variable is set. + var clientCertPath string + if v := env.Getenv(envPrefix + "CERT"); v != "" { + clientCertPath = v + } + // Next, check if the cloud entry sets a client cert. + if v := cloud.ClientCertFile; v != "" { + clientCertPath = v + } + + // Check if a custom client key was provided. + // First, check if the KEY environment variable is set. + var clientKeyPath string + if v := env.Getenv(envPrefix + "KEY"); v != "" { + clientKeyPath = v + } + // Next, check if the cloud entry sets a client key. + if v := cloud.ClientKeyFile; v != "" { + clientKeyPath = v + } + + // Define whether or not SSL API requests should be verified. + var insecurePtr *bool + if cloud.Verify != nil { + // Here we take the boolean pointer negation. + insecure := !*cloud.Verify + insecurePtr = &insecure + } + + tlsConfig, err := internal.PrepareTLSConfig(caCertPath, clientCertPath, clientKeyPath, insecurePtr) + if err != nil { + return nil, err + } + + // Get a Provider Client + ao, err := AuthOptions(opts) + if err != nil { + return nil, err + } + pClient, err := openstack.NewClient(ao.IdentityEndpoint) + if err != nil { + return nil, err + } + + // If an HTTPClient was specified, use it. + if opts.HTTPClient != nil { + pClient.HTTPClient = *opts.HTTPClient + } else { + // Otherwise create a new HTTP client with the generated TLS config. + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig = tlsConfig + pClient.HTTPClient = http.Client{Transport: transport} + } + + err = openstack.Authenticate(ctx, pClient, *ao) + if err != nil { + return nil, err + } + + // Determine the region to use. + // First, check if the REGION_NAME environment variable is set. + var region string + if v := env.Getenv(envPrefix + "REGION_NAME"); v != "" { + region = v + } + + // Next, check if the cloud entry sets a region. + if v := cloud.RegionName; v != "" { + region = v + } + + // Finally, see if one was specified in the ClientOpts. + // If so, this takes precedence. + if v := opts.RegionName; v != "" { + region = v + } + + // Determine the endpoint type to use. + // First, check if the OS_INTERFACE environment variable is set. + var endpointType string + if v := env.Getenv(envPrefix + "INTERFACE"); v != "" { + endpointType = v + } + + // Next, check if the cloud entry sets an endpoint type. + if v := cloud.EndpointType; v != "" { + endpointType = v + } + + // Finally, see if one was specified in the ClientOpts. + // If so, this takes precedence. + if v := opts.EndpointType; v != "" { + endpointType = v + } + + eo := gophercloud.EndpointOpts{ + Region: region, + Availability: GetEndpointType(endpointType), + } + + switch service { + case "baremetal": + return openstack.NewBareMetalV1(pClient, eo) + case "baremetal-introspection": + return openstack.NewBareMetalIntrospectionV1(pClient, eo) + case "compute": + return openstack.NewComputeV2(pClient, eo) + case "container": + return openstack.NewContainerV1(pClient, eo) + case "container-infra": + return openstack.NewContainerInfraV1(pClient, eo) + case "database": + return openstack.NewDBV1(pClient, eo) + case "dns": + return openstack.NewDNSV2(pClient, eo) + case "gnocchi": + return gnocchi.NewGnocchiV1(pClient, eo) + case "identity": + identityVersion := "3" + if v := cloud.IdentityAPIVersion; v != "" { + identityVersion = v + } + + switch identityVersion { + case "v2", "2", "2.0": + return openstack.NewIdentityV2(pClient, eo) + case "v3", "3": + return openstack.NewIdentityV3(pClient, eo) + default: + return nil, fmt.Errorf("invalid identity API version") + } + case "image": + return openstack.NewImageV2(pClient, eo) + case "key-manager": + return openstack.NewKeyManagerV1(pClient, eo) + case "load-balancer": + return openstack.NewLoadBalancerV2(pClient, eo) + case "messaging": + clientID, err := uuid.NewV4() + if err != nil { + return nil, fmt.Errorf("failed to generate UUID: %w", err) + } + return openstack.NewMessagingV2(pClient, clientID.String(), eo) + case "network": + return openstack.NewNetworkV2(pClient, eo) + case "object-store": + return openstack.NewObjectStorageV1(pClient, eo) + case "orchestration": + return openstack.NewOrchestrationV1(pClient, eo) + case "placement": + return openstack.NewPlacementV1(pClient, eo) + case "sharev2": + return openstack.NewSharedFileSystemV2(pClient, eo) + case "volume": + volumeVersion := "3" + if v := cloud.VolumeAPIVersion; v != "" { + volumeVersion = v + } + + switch volumeVersion { + case "v1", "1": + return openstack.NewBlockStorageV1(pClient, eo) + case "v2", "2": + return openstack.NewBlockStorageV2(pClient, eo) + case "v3", "3": + return openstack.NewBlockStorageV3(pClient, eo) + default: + return nil, fmt.Errorf("invalid volume API version") + } + case "workflowv2": + return openstack.NewWorkflowV2(pClient, eo) + } + + return nil, fmt.Errorf("unable to create a service client for %s", service) +} + +// isProjectScoped determines if an auth struct is project scoped. +func isProjectScoped(authInfo *AuthInfo) bool { + if authInfo.ProjectID == "" && authInfo.ProjectName == "" { + return false + } + + return true +} + +// setDomainIfNeeded will set a DomainID and DomainName +// to ProjectDomain* and UserDomain* if not already set. +func setDomainIfNeeded(cloud *Cloud) *Cloud { + if cloud.AuthInfo.DomainID != "" { + if cloud.AuthInfo.UserDomainID == "" { + cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DomainID + } + + if cloud.AuthInfo.ProjectDomainID == "" { + cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DomainID + } + + cloud.AuthInfo.DomainID = "" + } + + if cloud.AuthInfo.DomainName != "" { + if cloud.AuthInfo.UserDomainName == "" { + cloud.AuthInfo.UserDomainName = cloud.AuthInfo.DomainName + } + + if cloud.AuthInfo.ProjectDomainName == "" { + cloud.AuthInfo.ProjectDomainName = cloud.AuthInfo.DomainName + } + + cloud.AuthInfo.DomainName = "" + } + + // If Domain fields are still not set, and if DefaultDomain has a value, + // set UserDomainID and ProjectDomainID to DefaultDomain. + // https://github.com/openstack/osc-lib/blob/86129e6f88289ef14bfaa3f7c9cdfbea8d9fc944/osc_lib/cli/client_config.py#L117-L146 + if cloud.AuthInfo.DefaultDomain != "" { + if cloud.AuthInfo.UserDomainName == "" && cloud.AuthInfo.UserDomainID == "" { + cloud.AuthInfo.UserDomainID = cloud.AuthInfo.DefaultDomain + } + + if cloud.AuthInfo.ProjectDomainName == "" && cloud.AuthInfo.ProjectDomainID == "" { + cloud.AuthInfo.ProjectDomainID = cloud.AuthInfo.DefaultDomain + } + } + + return cloud +} + +// isApplicationCredential determines if an application credential is used to auth. +func isApplicationCredential(authInfo *AuthInfo) bool { + if authInfo.ApplicationCredentialID == "" && authInfo.ApplicationCredentialName == "" && authInfo.ApplicationCredentialSecret == "" { + return false + } + return true +} diff --git a/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/results.go b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/results.go new file mode 100644 index 000000000..6f3bd1166 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/results.go @@ -0,0 +1,187 @@ +package clientconfig + +import "encoding/json" + +// PublicClouds represents a collection of PublicCloud entries in clouds-public.yaml file. +// The format of the clouds-public.yml is documented at +// https://docs.openstack.org/python-openstackclient/latest/configuration/ +type PublicClouds struct { + Clouds map[string]Cloud `yaml:"public-clouds" json:"public-clouds"` +} + +// Clouds represents a collection of Cloud entries in a clouds.yaml file. +// The format of clouds.yaml is documented at +// https://docs.openstack.org/os-client-config/latest/user/configuration.html. +type Clouds struct { + Clouds map[string]Cloud `yaml:"clouds" json:"clouds"` +} + +// Cloud represents an entry in a clouds.yaml/public-clouds.yaml/secure.yaml file. +type Cloud struct { + Cloud string `yaml:"cloud,omitempty" json:"cloud,omitempty"` + Profile string `yaml:"profile,omitempty" json:"profile,omitempty"` + AuthInfo *AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"` + AuthType AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"` + RegionName string `yaml:"region_name,omitempty" json:"region_name,omitempty"` + Regions []Region `yaml:"regions,omitempty" json:"regions,omitempty"` + + // EndpointType and Interface both specify whether to use the public, internal, + // or admin interface of a service. They should be considered synonymous, but + // EndpointType will take precedence when both are specified. + EndpointType string `yaml:"endpoint_type,omitempty" json:"endpoint_type,omitempty"` + Interface string `yaml:"interface,omitempty" json:"interface,omitempty"` + + // API Version overrides. + IdentityAPIVersion string `yaml:"identity_api_version,omitempty" json:"identity_api_version,omitempty"` + VolumeAPIVersion string `yaml:"volume_api_version,omitempty" json:"volume_api_version,omitempty"` + + // Verify whether or not SSL API requests should be verified. + Verify *bool `yaml:"verify,omitempty" json:"verify,omitempty"` + + // CACertFile a path to a CA Cert bundle that can be used as part of + // verifying SSL API requests. + CACertFile string `yaml:"cacert,omitempty" json:"cacert,omitempty"` + + // ClientCertFile a path to a client certificate to use as part of the SSL + // transaction. + ClientCertFile string `yaml:"cert,omitempty" json:"cert,omitempty"` + + // ClientKeyFile a path to a client key to use as part of the SSL + // transaction. + ClientKeyFile string `yaml:"key,omitempty" json:"key,omitempty"` +} + +// AuthInfo represents the auth section of a cloud entry or +// auth options entered explicitly in ClientOpts. +type AuthInfo struct { + // AuthURL is the keystone/identity endpoint URL. + AuthURL string `yaml:"auth_url,omitempty" json:"auth_url,omitempty"` + + // Token is a pre-generated authentication token. + Token string `yaml:"token,omitempty" json:"token,omitempty"` + + // Username is the username of the user. + Username string `yaml:"username,omitempty" json:"username,omitempty"` + + // UserID is the unique ID of a user. + UserID string `yaml:"user_id,omitempty" json:"user_id,omitempty"` + + // Password is the password of the user. + Password string `yaml:"password,omitempty" json:"password,omitempty"` + + // Application Credential ID to login with. + ApplicationCredentialID string `yaml:"application_credential_id,omitempty" json:"application_credential_id,omitempty"` + + // Application Credential name to login with. + ApplicationCredentialName string `yaml:"application_credential_name,omitempty" json:"application_credential_name,omitempty"` + + // Application Credential secret to login with. + ApplicationCredentialSecret string `yaml:"application_credential_secret,omitempty" json:"application_credential_secret,omitempty"` + + // SystemScope is a system information to scope to. + SystemScope string `yaml:"system_scope,omitempty" json:"system_scope,omitempty"` + + // ProjectName is the common/human-readable name of a project. + // Users can be scoped to a project. + // ProjectName on its own is not enough to ensure a unique scope. It must + // also be combined with either a ProjectDomainName or ProjectDomainID. + // ProjectName cannot be combined with ProjectID in a scope. + ProjectName string `yaml:"project_name,omitempty" json:"project_name,omitempty"` + + // ProjectID is the unique ID of a project. + // It can be used to scope a user to a specific project. + ProjectID string `yaml:"project_id,omitempty" json:"project_id,omitempty"` + + // UserDomainName is the name of the domain where a user resides. + // It is used to identify the source domain of a user. + UserDomainName string `yaml:"user_domain_name,omitempty" json:"user_domain_name,omitempty"` + + // UserDomainID is the unique ID of the domain where a user resides. + // It is used to identify the source domain of a user. + UserDomainID string `yaml:"user_domain_id,omitempty" json:"user_domain_id,omitempty"` + + // ProjectDomainName is the name of the domain where a project resides. + // It is used to identify the source domain of a project. + // ProjectDomainName can be used in addition to a ProjectName when scoping + // a user to a specific project. + ProjectDomainName string `yaml:"project_domain_name,omitempty" json:"project_domain_name,omitempty"` + + // ProjectDomainID is the name of the domain where a project resides. + // It is used to identify the source domain of a project. + // ProjectDomainID can be used in addition to a ProjectName when scoping + // a user to a specific project. + ProjectDomainID string `yaml:"project_domain_id,omitempty" json:"project_domain_id,omitempty"` + + // DomainName is the name of a domain which can be used to identify the + // source domain of either a user or a project. + // If UserDomainName and ProjectDomainName are not specified, then DomainName + // is used as a default choice. + // It can also be used be used to specify a domain-only scope. + DomainName string `yaml:"domain_name,omitempty" json:"domain_name,omitempty"` + + // DomainID is the unique ID of a domain which can be used to identify the + // source domain of eitehr a user or a project. + // If UserDomainID and ProjectDomainID are not specified, then DomainID is + // used as a default choice. + // It can also be used be used to specify a domain-only scope. + DomainID string `yaml:"domain_id,omitempty" json:"domain_id,omitempty"` + + // DefaultDomain is the domain ID to fall back on if no other domain has + // been specified and a domain is required for scope. + DefaultDomain string `yaml:"default_domain,omitempty" json:"default_domain,omitempty"` + + // AllowReauth should be set to true if you grant permission for Gophercloud to + // cache your credentials in memory, and to allow Gophercloud to attempt to + // re-authenticate automatically if/when your token expires. If you set it to + // false, it will not cache these settings, but re-authentication will not be + // possible. This setting defaults to false. + AllowReauth bool `yaml:"allow_reauth,omitempty" json:"allow_reauth,omitempty"` +} + +// Region represents a region included as part of cloud in clouds.yaml +// According to Python-based openstacksdk, this can be either a struct (as defined) +// or a plain string. Custom unmarshallers handle both cases. +type Region struct { + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Values Cloud `yaml:"values,omitempty" json:"values,omitempty"` +} + +// UnmarshalJSON handles either a plain string acting as the Name property or +// a struct, mimicking the Python-based openstacksdk. +func (r *Region) UnmarshalJSON(data []byte) error { + var name string + if err := json.Unmarshal(data, &name); err == nil { + r.Name = name + return nil + } + + type region Region + var tmp region + if err := json.Unmarshal(data, &tmp); err != nil { + return err + } + r.Name = tmp.Name + r.Values = tmp.Values + + return nil +} + +// UnmarshalYAML handles either a plain string acting as the Name property or +// a struct, mimicking the Python-based openstacksdk. +func (r *Region) UnmarshalYAML(unmarshal func(interface{}) error) error { + var name string + if err := unmarshal(&name); err == nil { + r.Name = name + return nil + } + + type region Region + var tmp region + if err := unmarshal(&tmp); err != nil { + return err + } + r.Name = tmp.Name + r.Values = tmp.Values + + return nil +} diff --git a/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/utils.go b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/utils.go new file mode 100644 index 000000000..ec8a77a61 --- /dev/null +++ b/vendor/github.com/gophercloud/utils/v2/openstack/clientconfig/utils.go @@ -0,0 +1,193 @@ +package clientconfig + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "os/user" + "path/filepath" + "reflect" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/utils/v2/env" +) + +// defaultIfEmpty is a helper function to make it cleaner to set default value +// for strings. +func defaultIfEmpty(value string, defaultValue string) string { + if value == "" { + return defaultValue + } + return value +} + +// mergeCLouds merges two Clouds recursively (the AuthInfo also gets merged). +// In case both Clouds define a value, the value in the 'override' cloud takes precedence +func mergeClouds(override, cloud interface{}) (*Cloud, error) { + overrideJson, err := json.Marshal(override) + if err != nil { + return nil, err + } + cloudJson, err := json.Marshal(cloud) + if err != nil { + return nil, err + } + var overrideInterface interface{} + err = json.Unmarshal(overrideJson, &overrideInterface) + if err != nil { + return nil, err + } + var cloudInterface interface{} + err = json.Unmarshal(cloudJson, &cloudInterface) + if err != nil { + return nil, err + } + var mergedCloud Cloud + mergedInterface := mergeInterfaces(overrideInterface, cloudInterface) + mergedJson, err := json.Marshal(mergedInterface) + if err != nil { + return nil, err + } + err = json.Unmarshal(mergedJson, &mergedCloud) + if err != nil { + return nil, err + } + return &mergedCloud, nil +} + +// merges two interfaces. In cases where a value is defined for both 'overridingInterface' and +// 'inferiorInterface' the value in 'overridingInterface' will take precedence. +func mergeInterfaces(overridingInterface, inferiorInterface interface{}) interface{} { + switch overriding := overridingInterface.(type) { + case map[string]interface{}: + interfaceMap, ok := inferiorInterface.(map[string]interface{}) + if !ok { + return overriding + } + for k, v := range interfaceMap { + if overridingValue, ok := overriding[k]; ok { + overriding[k] = mergeInterfaces(overridingValue, v) + } else { + overriding[k] = v + } + } + case []interface{}: + list, ok := inferiorInterface.([]interface{}) + if !ok { + return overriding + } + for i := range list { + overriding = append(overriding, list[i]) + } + return overriding + case nil: + // mergeClouds(nil, map[string]interface{...}) -> map[string]interface{...} + v, ok := inferiorInterface.(map[string]interface{}) + if ok { + return v + } + } + // We don't want to override with empty values + if reflect.DeepEqual(overridingInterface, nil) || reflect.DeepEqual(reflect.Zero(reflect.TypeOf(overridingInterface)).Interface(), overridingInterface) { + return inferiorInterface + } else { + return overridingInterface + } +} + +// FindAndReadCloudsYAML attempts to locate a clouds.yaml file in the following +// locations: +// +// 1. OS_CLIENT_CONFIG_FILE +// 2. Current directory. +// 3. unix-specific user_config_dir (~/.config/openstack/clouds.yaml) +// 4. unix-specific site_config_dir (/etc/openstack/clouds.yaml) +// +// If found, the contents of the file is returned. +func FindAndReadCloudsYAML() (string, []byte, error) { + // OS_CLIENT_CONFIG_FILE + if v := env.Getenv("OS_CLIENT_CONFIG_FILE"); v != "" { + if ok := fileExists(v); ok { + content, err := ioutil.ReadFile(v) + return v, content, err + } + } + + s, b, err := FindAndReadYAML("clouds.yaml") + if s == "" { + return FindAndReadYAML("clouds.yml") + } + return s, b, err +} + +func FindAndReadPublicCloudsYAML() (string, []byte, error) { + s, b, err := FindAndReadYAML("clouds-public.yaml") + if s == "" { + return FindAndReadYAML("clouds-public.yml") + } + return s, b, err +} + +func FindAndReadSecureCloudsYAML() (string, []byte, error) { + s, b, err := FindAndReadYAML("secure.yaml") + if s == "" { + return FindAndReadYAML("secure.yml") + } + return s, b, err +} + +func FindAndReadYAML(yamlFile string) (string, []byte, error) { + // current directory + cwd, err := os.Getwd() + if err != nil { + return "", nil, fmt.Errorf("unable to determine working directory: %w", err) + } + + filename := filepath.Join(cwd, yamlFile) + if ok := fileExists(filename); ok { + content, err := ioutil.ReadFile(filename) + return filename, content, err + } + + // unix user config directory: ~/.config/openstack. + if currentUser, err := user.Current(); err == nil { + homeDir := currentUser.HomeDir + if homeDir != "" { + filename := filepath.Join(homeDir, ".config/openstack/"+yamlFile) + if ok := fileExists(filename); ok { + content, err := ioutil.ReadFile(filename) + return filename, content, err + } + } + } + + // unix-specific site config directory: /etc/openstack. + filename = "/etc/openstack/" + yamlFile + if ok := fileExists(filename); ok { + content, err := ioutil.ReadFile(filename) + return filename, content, err + } + + return "", nil, fmt.Errorf("no %s file found: %w", yamlFile, os.ErrNotExist) +} + +// fileExists checks for the existence of a file at a given location. +func fileExists(filename string) bool { + if _, err := os.Stat(filename); err == nil { + return true + } + return false +} + +// GetEndpointType is a helper method to determine the endpoint type +// requested by the user. +func GetEndpointType(endpointType string) gophercloud.Availability { + if endpointType == "internal" || endpointType == "internalURL" { + return gophercloud.AvailabilityInternal + } + if endpointType == "admin" || endpointType == "adminURL" { + return gophercloud.AvailabilityAdmin + } + return gophercloud.AvailabilityPublic +} diff --git a/vendor/go.uber.org/mock/AUTHORS b/vendor/go.uber.org/mock/AUTHORS new file mode 100644 index 000000000..660b8ccc8 --- /dev/null +++ b/vendor/go.uber.org/mock/AUTHORS @@ -0,0 +1,12 @@ +# This is the official list of GoMock authors for copyright purposes. +# This file is distinct from the CONTRIBUTORS files. +# See the latter for an explanation. + +# Names should be added to this file as +# Name or Organization +# The email address is not required for organizations. + +# Please keep the list sorted. + +Alex Reece +Google Inc. diff --git a/vendor/go.uber.org/mock/LICENSE b/vendor/go.uber.org/mock/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/vendor/go.uber.org/mock/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/go.uber.org/mock/gomock/call.go b/vendor/go.uber.org/mock/gomock/call.go new file mode 100644 index 000000000..e1fe222af --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/call.go @@ -0,0 +1,506 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +// Call represents an expected call to a mock. +type Call struct { + t TestHelper // for triggering test failures on invalid call setup + + receiver any // the receiver of the method call + method string // the name of the method + methodType reflect.Type // the type of the method + args []Matcher // the args + origin string // file and line number of call setup + + preReqs []*Call // prerequisite calls + + // Expectations + minCalls, maxCalls int + + numCalls int // actual number made + + // actions are called when this Call is called. Each action gets the args and + // can set the return values by returning a non-nil slice. Actions run in the + // order they are created. + actions []func([]any) []any +} + +// newCall creates a *Call. It requires the method type in order to support +// unexported methods. +func newCall(t TestHelper, receiver any, method string, methodType reflect.Type, args ...any) *Call { + t.Helper() + + // TODO: check arity, types. + mArgs := make([]Matcher, len(args)) + for i, arg := range args { + if m, ok := arg.(Matcher); ok { + mArgs[i] = m + } else if arg == nil { + // Handle nil specially so that passing a nil interface value + // will match the typed nils of concrete args. + mArgs[i] = Nil() + } else { + mArgs[i] = Eq(arg) + } + } + + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is RecordCallWithMethodType(), 2 is the generated recorder, and 3 is the user's test. + origin := callerInfo(3) + actions := []func([]any) []any{func([]any) []any { + // Synthesize the zero value for each of the return args' types. + rets := make([]any, methodType.NumOut()) + for i := 0; i < methodType.NumOut(); i++ { + rets[i] = reflect.Zero(methodType.Out(i)).Interface() + } + return rets + }} + return &Call{ + t: t, receiver: receiver, method: method, methodType: methodType, + args: mArgs, origin: origin, minCalls: 1, maxCalls: 1, actions: actions, + } +} + +// AnyTimes allows the expectation to be called 0 or more times +func (c *Call) AnyTimes() *Call { + c.minCalls, c.maxCalls = 0, 1e8 // close enough to infinity + return c +} + +// MinTimes requires the call to occur at least n times. If AnyTimes or MaxTimes have not been called or if MaxTimes +// was previously called with 1, MinTimes also sets the maximum number of calls to infinity. +func (c *Call) MinTimes(n int) *Call { + c.minCalls = n + if c.maxCalls == 1 { + c.maxCalls = 1e8 + } + return c +} + +// MaxTimes limits the number of calls to n times. If AnyTimes or MinTimes have not been called or if MinTimes was +// previously called with 1, MaxTimes also sets the minimum number of calls to 0. +func (c *Call) MaxTimes(n int) *Call { + c.maxCalls = n + if c.minCalls == 1 { + c.minCalls = 0 + } + return c +} + +// DoAndReturn declares the action to run when the call is matched. +// The return values from this function are returned by the mocked function. +// It takes an any argument to support n-arity functions. +// The anonymous function must match the function signature mocked method. +func (c *Call) DoAndReturn(f any) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []any) []any { + c.t.Helper() + ft := v.Type() + if c.methodType.NumIn() != ft.NumIn() { + if ft.IsVariadic() { + c.t.Fatalf("wrong number of arguments in DoAndReturn func for %T.%v The function signature must match the mocked method, a variadic function cannot be used.", + c.receiver, c.method) + } else { + c.t.Fatalf("wrong number of arguments in DoAndReturn func for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, ft.NumIn(), c.methodType.NumIn(), c.origin) + } + return nil + } + vArgs := make([]reflect.Value, len(args)) + for i := 0; i < len(args); i++ { + if args[i] != nil { + vArgs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vArgs[i] = reflect.Zero(ft.In(i)) + } + } + vRets := v.Call(vArgs) + rets := make([]any, len(vRets)) + for i, ret := range vRets { + rets[i] = ret.Interface() + } + return rets + }) + return c +} + +// Do declares the action to run when the call is matched. The function's +// return values are ignored to retain backward compatibility. To use the +// return values call DoAndReturn. +// It takes an any argument to support n-arity functions. +// The anonymous function must match the function signature mocked method. +func (c *Call) Do(f any) *Call { + // TODO: Check arity and types here, rather than dying badly elsewhere. + v := reflect.ValueOf(f) + + c.addAction(func(args []any) []any { + c.t.Helper() + ft := v.Type() + if c.methodType.NumIn() != ft.NumIn() { + if ft.IsVariadic() { + c.t.Fatalf("wrong number of arguments in Do func for %T.%v The function signature must match the mocked method, a variadic function cannot be used.", + c.receiver, c.method) + } else { + c.t.Fatalf("wrong number of arguments in Do func for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, ft.NumIn(), c.methodType.NumIn(), c.origin) + } + return nil + } + vArgs := make([]reflect.Value, len(args)) + for i := 0; i < len(args); i++ { + if args[i] != nil { + vArgs[i] = reflect.ValueOf(args[i]) + } else { + // Use the zero value for the arg. + vArgs[i] = reflect.Zero(ft.In(i)) + } + } + v.Call(vArgs) + return nil + }) + return c +} + +// Return declares the values to be returned by the mocked function call. +func (c *Call) Return(rets ...any) *Call { + c.t.Helper() + + mt := c.methodType + if len(rets) != mt.NumOut() { + c.t.Fatalf("wrong number of arguments to Return for %T.%v: got %d, want %d [%s]", + c.receiver, c.method, len(rets), mt.NumOut(), c.origin) + } + for i, ret := range rets { + if got, want := reflect.TypeOf(ret), mt.Out(i); got == want { + // Identical types; nothing to do. + } else if got == nil { + // Nil needs special handling. + switch want.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: + // ok + default: + c.t.Fatalf("argument %d to Return for %T.%v is nil, but %v is not nillable [%s]", + i, c.receiver, c.method, want, c.origin) + } + } else if got.AssignableTo(want) { + // Assignable type relation. Make the assignment now so that the generated code + // can return the values with a type assertion. + v := reflect.New(want).Elem() + v.Set(reflect.ValueOf(ret)) + rets[i] = v.Interface() + } else { + c.t.Fatalf("wrong type of argument %d to Return for %T.%v: %v is not assignable to %v [%s]", + i, c.receiver, c.method, got, want, c.origin) + } + } + + c.addAction(func([]any) []any { + return rets + }) + + return c +} + +// Times declares the exact number of times a function call is expected to be executed. +func (c *Call) Times(n int) *Call { + c.minCalls, c.maxCalls = n, n + return c +} + +// SetArg declares an action that will set the nth argument's value, +// indirected through a pointer. Or, in the case of a slice and map, SetArg +// will copy value's elements/key-value pairs into the nth argument. +func (c *Call) SetArg(n int, value any) *Call { + c.t.Helper() + + mt := c.methodType + // TODO: This will break on variadic methods. + // We will need to check those at invocation time. + if n < 0 || n >= mt.NumIn() { + c.t.Fatalf("SetArg(%d, ...) called for a method with %d args [%s]", + n, mt.NumIn(), c.origin) + } + // Permit setting argument through an interface. + // In the interface case, we don't (nay, can't) check the type here. + at := mt.In(n) + switch at.Kind() { + case reflect.Ptr: + dt := at.Elem() + if vt := reflect.TypeOf(value); !vt.AssignableTo(dt) { + c.t.Fatalf("SetArg(%d, ...) argument is a %v, not assignable to %v [%s]", + n, vt, dt, c.origin) + } + case reflect.Interface, reflect.Slice, reflect.Map: + // nothing to do + default: + c.t.Fatalf("SetArg(%d, ...) referring to argument of non-pointer non-interface non-slice non-map type %v [%s]", + n, at, c.origin) + } + + c.addAction(func(args []any) []any { + v := reflect.ValueOf(value) + switch reflect.TypeOf(args[n]).Kind() { + case reflect.Slice: + setSlice(args[n], v) + case reflect.Map: + setMap(args[n], v) + default: + reflect.ValueOf(args[n]).Elem().Set(v) + } + return nil + }) + return c +} + +// isPreReq returns true if other is a direct or indirect prerequisite to c. +func (c *Call) isPreReq(other *Call) bool { + for _, preReq := range c.preReqs { + if other == preReq || preReq.isPreReq(other) { + return true + } + } + return false +} + +// After declares that the call may only match after preReq has been exhausted. +func (c *Call) After(preReq *Call) *Call { + c.t.Helper() + + if c == preReq { + c.t.Fatalf("A call isn't allowed to be its own prerequisite") + } + if preReq.isPreReq(c) { + c.t.Fatalf("Loop in call order: %v is a prerequisite to %v (possibly indirectly).", c, preReq) + } + + c.preReqs = append(c.preReqs, preReq) + return c +} + +// Returns true if the minimum number of calls have been made. +func (c *Call) satisfied() bool { + return c.numCalls >= c.minCalls +} + +// Returns true if the maximum number of calls have been made. +func (c *Call) exhausted() bool { + return c.numCalls >= c.maxCalls +} + +func (c *Call) String() string { + args := make([]string, len(c.args)) + for i, arg := range c.args { + args[i] = arg.String() + } + arguments := strings.Join(args, ", ") + return fmt.Sprintf("%T.%v(%s) %s", c.receiver, c.method, arguments, c.origin) +} + +// Tests if the given call matches the expected call. +// If yes, returns nil. If no, returns error with message explaining why it does not match. +func (c *Call) matches(args []any) error { + if !c.methodType.IsVariadic() { + if len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + + for i, m := range c.args { + if !m.Matches(args[i]) { + return fmt.Errorf( + "expected call at %s doesn't match the argument at index %d.\nGot: %v\nWant: %v", + c.origin, i, formatGottenArg(m, args[i]), m, + ) + } + } + } else { + if len(c.args) < c.methodType.NumIn()-1 { + return fmt.Errorf("expected call at %s has the wrong number of matchers. Got: %d, want: %d", + c.origin, len(c.args), c.methodType.NumIn()-1) + } + if len(c.args) != c.methodType.NumIn() && len(args) != len(c.args) { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: %d", + c.origin, len(args), len(c.args)) + } + if len(args) < len(c.args)-1 { + return fmt.Errorf("expected call at %s has the wrong number of arguments. Got: %d, want: greater than or equal to %d", + c.origin, len(args), len(c.args)-1) + } + + for i, m := range c.args { + if i < c.methodType.NumIn()-1 { + // Non-variadic args + if !m.Matches(args[i]) { + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i]), m) + } + continue + } + // The last arg has a possibility of a variadic argument, so let it branch + + // sample: Foo(a int, b int, c ...int) + if i < len(c.args) && i < len(args) { + if m.Matches(args[i]) { + // Got Foo(a, b, c) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC) + // Got Foo(a, b) want Foo(matcherA, matcherB) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD) + continue + } + } + + // The number of actual args don't match the number of matchers, + // or the last matcher is a slice and the last arg is not. + // If this function still matches it is because the last matcher + // matches all the remaining arguments or the lack of any. + // Convert the remaining arguments, if any, into a slice of the + // expected type. + vArgsType := c.methodType.In(c.methodType.NumIn() - 1) + vArgs := reflect.MakeSlice(vArgsType, 0, len(args)-i) + for _, arg := range args[i:] { + vArgs = reflect.Append(vArgs, reflect.ValueOf(arg)) + } + if m.Matches(vArgs.Interface()) { + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, someSliceMatcher) + // Got Foo(a, b) want Foo(matcherA, matcherB, gomock.Any()) + // Got Foo(a, b) want Foo(matcherA, matcherB, someEmptySliceMatcher) + break + } + // Wrong number of matchers or not match. Fail. + // Got Foo(a, b) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c, d) want Foo(matcherA, matcherB, matcherC, matcherD, matcherE) + // Got Foo(a, b, c, d, e) want Foo(matcherA, matcherB, matcherC, matcherD) + // Got Foo(a, b, c) want Foo(matcherA, matcherB) + + return fmt.Errorf("expected call at %s doesn't match the argument at index %s.\nGot: %v\nWant: %v", + c.origin, strconv.Itoa(i), formatGottenArg(m, args[i:]), c.args[i]) + } + } + + // Check that all prerequisite calls have been satisfied. + for _, preReqCall := range c.preReqs { + if !preReqCall.satisfied() { + return fmt.Errorf("expected call at %s doesn't have a prerequisite call satisfied:\n%v\nshould be called before:\n%v", + c.origin, preReqCall, c) + } + } + + // Check that the call is not exhausted. + if c.exhausted() { + return fmt.Errorf("expected call at %s has already been called the max number of times", c.origin) + } + + return nil +} + +// dropPrereqs tells the expected Call to not re-check prerequisite calls any +// longer, and to return its current set. +func (c *Call) dropPrereqs() (preReqs []*Call) { + preReqs = c.preReqs + c.preReqs = nil + return +} + +func (c *Call) call() []func([]any) []any { + c.numCalls++ + return c.actions +} + +// InOrder declares that the given calls should occur in order. +// It panics if the type of any of the arguments isn't *Call or a generated +// mock with an embedded *Call. +func InOrder(args ...any) { + calls := make([]*Call, 0, len(args)) + for i := 0; i < len(args); i++ { + if call := getCall(args[i]); call != nil { + calls = append(calls, call) + continue + } + panic(fmt.Sprintf( + "invalid argument at position %d of type %T, InOrder expects *gomock.Call or generated mock types with an embedded *gomock.Call", + i, + args[i], + )) + } + for i := 1; i < len(calls); i++ { + calls[i].After(calls[i-1]) + } +} + +// getCall checks if the parameter is a *Call or a generated struct +// that wraps a *Call and returns the *Call pointer - if neither, it returns nil. +func getCall(arg any) *Call { + if call, ok := arg.(*Call); ok { + return call + } + t := reflect.ValueOf(arg) + if t.Kind() != reflect.Ptr && t.Kind() != reflect.Interface { + return nil + } + t = t.Elem() + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !f.CanInterface() { + continue + } + if call, ok := f.Interface().(*Call); ok { + return call + } + } + return nil +} + +func setSlice(arg any, v reflect.Value) { + va := reflect.ValueOf(arg) + for i := 0; i < v.Len(); i++ { + va.Index(i).Set(v.Index(i)) + } +} + +func setMap(arg any, v reflect.Value) { + va := reflect.ValueOf(arg) + for _, e := range va.MapKeys() { + va.SetMapIndex(e, reflect.Value{}) + } + for _, e := range v.MapKeys() { + va.SetMapIndex(e, v.MapIndex(e)) + } +} + +func (c *Call) addAction(action func([]any) []any) { + c.actions = append(c.actions, action) +} + +func formatGottenArg(m Matcher, arg any) string { + got := fmt.Sprintf("%v (%T)", arg, arg) + if gs, ok := m.(GotFormatter); ok { + got = gs.Got(arg) + } + return got +} diff --git a/vendor/go.uber.org/mock/gomock/callset.go b/vendor/go.uber.org/mock/gomock/callset.go new file mode 100644 index 000000000..f5cc592d6 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/callset.go @@ -0,0 +1,164 @@ +// Copyright 2011 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "bytes" + "errors" + "fmt" + "sync" +) + +// callSet represents a set of expected calls, indexed by receiver and method +// name. +type callSet struct { + // Calls that are still expected. + expected map[callSetKey][]*Call + expectedMu *sync.Mutex + // Calls that have been exhausted. + exhausted map[callSetKey][]*Call + // when set to true, existing call expectations are overridden when new call expectations are made + allowOverride bool +} + +// callSetKey is the key in the maps in callSet +type callSetKey struct { + receiver any + fname string +} + +func newCallSet() *callSet { + return &callSet{ + expected: make(map[callSetKey][]*Call), + expectedMu: &sync.Mutex{}, + exhausted: make(map[callSetKey][]*Call), + } +} + +func newOverridableCallSet() *callSet { + return &callSet{ + expected: make(map[callSetKey][]*Call), + expectedMu: &sync.Mutex{}, + exhausted: make(map[callSetKey][]*Call), + allowOverride: true, + } +} + +// Add adds a new expected call. +func (cs callSet) Add(call *Call) { + key := callSetKey{call.receiver, call.method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + m := cs.expected + if call.exhausted() { + m = cs.exhausted + } + if cs.allowOverride { + m[key] = make([]*Call, 0) + } + + m[key] = append(m[key], call) +} + +// Remove removes an expected call. +func (cs callSet) Remove(call *Call) { + key := callSetKey{call.receiver, call.method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + calls := cs.expected[key] + for i, c := range calls { + if c == call { + // maintain order for remaining calls + cs.expected[key] = append(calls[:i], calls[i+1:]...) + cs.exhausted[key] = append(cs.exhausted[key], call) + break + } + } +} + +// FindMatch searches for a matching call. Returns error with explanation message if no call matched. +func (cs callSet) FindMatch(receiver any, method string, args []any) (*Call, error) { + key := callSetKey{receiver, method} + + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + // Search through the expected calls. + expected := cs.expected[key] + var callsErrors bytes.Buffer + for _, call := range expected { + err := call.matches(args) + if err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + } else { + return call, nil + } + } + + // If we haven't found a match then search through the exhausted calls so we + // get useful error messages. + exhausted := cs.exhausted[key] + for _, call := range exhausted { + if err := call.matches(args); err != nil { + _, _ = fmt.Fprintf(&callsErrors, "\n%v", err) + continue + } + _, _ = fmt.Fprintf( + &callsErrors, "all expected calls for method %q have been exhausted", method, + ) + } + + if len(expected)+len(exhausted) == 0 { + _, _ = fmt.Fprintf(&callsErrors, "there are no expected calls of the method %q for that receiver", method) + } + + return nil, errors.New(callsErrors.String()) +} + +// Failures returns the calls that are not satisfied. +func (cs callSet) Failures() []*Call { + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + failures := make([]*Call, 0, len(cs.expected)) + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + failures = append(failures, call) + } + } + } + return failures +} + +// Satisfied returns true in case all expected calls in this callSet are satisfied. +func (cs callSet) Satisfied() bool { + cs.expectedMu.Lock() + defer cs.expectedMu.Unlock() + + for _, calls := range cs.expected { + for _, call := range calls { + if !call.satisfied() { + return false + } + } + } + + return true +} diff --git a/vendor/go.uber.org/mock/gomock/controller.go b/vendor/go.uber.org/mock/gomock/controller.go new file mode 100644 index 000000000..674c3298c --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/controller.go @@ -0,0 +1,326 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "context" + "fmt" + "reflect" + "runtime" + "sync" +) + +// A TestReporter is something that can be used to report test failures. It +// is satisfied by the standard library's *testing.T. +type TestReporter interface { + Errorf(format string, args ...any) + Fatalf(format string, args ...any) +} + +// TestHelper is a TestReporter that has the Helper method. It is satisfied +// by the standard library's *testing.T. +type TestHelper interface { + TestReporter + Helper() +} + +// cleanuper is used to check if TestHelper also has the `Cleanup` method. A +// common pattern is to pass in a `*testing.T` to +// `NewController(t TestReporter)`. In Go 1.14+, `*testing.T` has a cleanup +// method. This can be utilized to call `Finish()` so the caller of this library +// does not have to. +type cleanuper interface { + Cleanup(func()) +} + +// A Controller represents the top-level control of a mock ecosystem. It +// defines the scope and lifetime of mock objects, as well as their +// expectations. It is safe to call Controller's methods from multiple +// goroutines. Each test should create a new Controller. +// +// func TestFoo(t *testing.T) { +// ctrl := gomock.NewController(t) +// // .. +// } +// +// func TestBar(t *testing.T) { +// t.Run("Sub-Test-1", st) { +// ctrl := gomock.NewController(st) +// // .. +// }) +// t.Run("Sub-Test-2", st) { +// ctrl := gomock.NewController(st) +// // .. +// }) +// }) +type Controller struct { + // T should only be called within a generated mock. It is not intended to + // be used in user code and may be changed in future versions. T is the + // TestReporter passed in when creating the Controller via NewController. + // If the TestReporter does not implement a TestHelper it will be wrapped + // with a nopTestHelper. + T TestHelper + mu sync.Mutex + expectedCalls *callSet + finished bool +} + +// NewController returns a new Controller. It is the preferred way to create a Controller. +// +// Passing [*testing.T] registers cleanup function to automatically call [Controller.Finish] +// when the test and all its subtests complete. +func NewController(t TestReporter, opts ...ControllerOption) *Controller { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t} + } + ctrl := &Controller{ + T: h, + expectedCalls: newCallSet(), + } + for _, opt := range opts { + opt.apply(ctrl) + } + if c, ok := isCleanuper(ctrl.T); ok { + c.Cleanup(func() { + ctrl.T.Helper() + ctrl.finish(true, nil) + }) + } + + return ctrl +} + +// ControllerOption configures how a Controller should behave. +type ControllerOption interface { + apply(*Controller) +} + +type overridableExpectationsOption struct{} + +// WithOverridableExpectations allows for overridable call expectations +// i.e., subsequent call expectations override existing call expectations +func WithOverridableExpectations() overridableExpectationsOption { + return overridableExpectationsOption{} +} + +func (o overridableExpectationsOption) apply(ctrl *Controller) { + ctrl.expectedCalls = newOverridableCallSet() +} + +type cancelReporter struct { + t TestHelper + cancel func() +} + +func (r *cancelReporter) Errorf(format string, args ...any) { + r.t.Errorf(format, args...) +} + +func (r *cancelReporter) Fatalf(format string, args ...any) { + defer r.cancel() + r.t.Fatalf(format, args...) +} + +func (r *cancelReporter) Helper() { + r.t.Helper() +} + +// WithContext returns a new Controller and a Context, which is cancelled on any +// fatal failure. +func WithContext(ctx context.Context, t TestReporter) (*Controller, context.Context) { + h, ok := t.(TestHelper) + if !ok { + h = &nopTestHelper{t: t} + } + + ctx, cancel := context.WithCancel(ctx) + return NewController(&cancelReporter{t: h, cancel: cancel}), ctx +} + +type nopTestHelper struct { + t TestReporter +} + +func (h *nopTestHelper) Errorf(format string, args ...any) { + h.t.Errorf(format, args...) +} + +func (h *nopTestHelper) Fatalf(format string, args ...any) { + h.t.Fatalf(format, args...) +} + +func (h nopTestHelper) Helper() {} + +// RecordCall is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCall(receiver any, method string, args ...any) *Call { + ctrl.T.Helper() + + recv := reflect.ValueOf(receiver) + for i := 0; i < recv.Type().NumMethod(); i++ { + if recv.Type().Method(i).Name == method { + return ctrl.RecordCallWithMethodType(receiver, method, recv.Method(i).Type(), args...) + } + } + ctrl.T.Fatalf("gomock: failed finding method %s on %T", method, receiver) + panic("unreachable") +} + +// RecordCallWithMethodType is called by a mock. It should not be called by user code. +func (ctrl *Controller) RecordCallWithMethodType(receiver any, method string, methodType reflect.Type, args ...any) *Call { + ctrl.T.Helper() + + call := newCall(ctrl.T, receiver, method, methodType, args...) + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + ctrl.expectedCalls.Add(call) + + return call +} + +// Call is called by a mock. It should not be called by user code. +func (ctrl *Controller) Call(receiver any, method string, args ...any) []any { + ctrl.T.Helper() + + // Nest this code so we can use defer to make sure the lock is released. + actions := func() []func([]any) []any { + ctrl.T.Helper() + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + expected, err := ctrl.expectedCalls.FindMatch(receiver, method, args) + if err != nil { + // callerInfo's skip should be updated if the number of calls between the user's test + // and this line changes, i.e. this code is wrapped in another anonymous function. + // 0 is us, 1 is controller.Call(), 2 is the generated mock, and 3 is the user's test. + origin := callerInfo(3) + stringArgs := make([]string, len(args)) + for i, arg := range args { + stringArgs[i] = getString(arg) + } + ctrl.T.Fatalf("Unexpected call to %T.%v(%v) at %s because: %s", receiver, method, stringArgs, origin, err) + } + + // Two things happen here: + // * the matching call no longer needs to check prerequisite calls, + // * and the prerequisite calls are no longer expected, so remove them. + preReqCalls := expected.dropPrereqs() + for _, preReqCall := range preReqCalls { + ctrl.expectedCalls.Remove(preReqCall) + } + + actions := expected.call() + if expected.exhausted() { + ctrl.expectedCalls.Remove(expected) + } + return actions + }() + + var rets []any + for _, action := range actions { + if r := action(args); r != nil { + rets = r + } + } + + return rets +} + +// Finish checks to see if all the methods that were expected to be called were called. +// It is not idempotent and therefore can only be invoked once. +// +// Note: If you pass a *testing.T into [NewController], you no longer +// need to call ctrl.Finish() in your test methods. +func (ctrl *Controller) Finish() { + // If we're currently panicking, probably because this is a deferred call. + // This must be recovered in the deferred function. + err := recover() + ctrl.finish(false, err) +} + +// Satisfied returns whether all expected calls bound to this Controller have been satisfied. +// Calling Finish is then guaranteed to not fail due to missing calls. +func (ctrl *Controller) Satisfied() bool { + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + return ctrl.expectedCalls.Satisfied() +} + +func (ctrl *Controller) finish(cleanup bool, panicErr any) { + ctrl.T.Helper() + + ctrl.mu.Lock() + defer ctrl.mu.Unlock() + + if ctrl.finished { + if _, ok := isCleanuper(ctrl.T); !ok { + ctrl.T.Fatalf("Controller.Finish was called more than once. It has to be called exactly once.") + } + return + } + ctrl.finished = true + + // Short-circuit, pass through the panic. + if panicErr != nil { + panic(panicErr) + } + + // Check that all remaining expected calls are satisfied. + failures := ctrl.expectedCalls.Failures() + for _, call := range failures { + ctrl.T.Errorf("missing call(s) to %v", call) + } + if len(failures) != 0 { + if !cleanup { + ctrl.T.Fatalf("aborting test due to missing call(s)") + return + } + ctrl.T.Errorf("aborting test due to missing call(s)") + } +} + +// callerInfo returns the file:line of the call site. skip is the number +// of stack frames to skip when reporting. 0 is callerInfo's call site. +func callerInfo(skip int) string { + if _, file, line, ok := runtime.Caller(skip + 1); ok { + return fmt.Sprintf("%s:%d", file, line) + } + return "unknown file" +} + +// isCleanuper checks it if t's base TestReporter has a Cleanup method. +func isCleanuper(t TestReporter) (cleanuper, bool) { + tr := unwrapTestReporter(t) + c, ok := tr.(cleanuper) + return c, ok +} + +// unwrapTestReporter unwraps TestReporter to the base implementation. +func unwrapTestReporter(t TestReporter) TestReporter { + tr := t + switch nt := t.(type) { + case *cancelReporter: + tr = nt.t + if h, check := tr.(*nopTestHelper); check { + tr = h.t + } + case *nopTestHelper: + tr = nt.t + default: + // not wrapped + } + return tr +} diff --git a/vendor/go.uber.org/mock/gomock/doc.go b/vendor/go.uber.org/mock/gomock/doc.go new file mode 100644 index 000000000..696dda388 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/doc.go @@ -0,0 +1,60 @@ +// Copyright 2022 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package gomock is a mock framework for Go. +// +// Standard usage: +// +// (1) Define an interface that you wish to mock. +// type MyInterface interface { +// SomeMethod(x int64, y string) +// } +// (2) Use mockgen to generate a mock from the interface. +// (3) Use the mock in a test: +// func TestMyThing(t *testing.T) { +// mockCtrl := gomock.NewController(t) +// mockObj := something.NewMockMyInterface(mockCtrl) +// mockObj.EXPECT().SomeMethod(4, "blah") +// // pass mockObj to a real object and play with it. +// } +// +// By default, expected calls are not enforced to run in any particular order. +// Call order dependency can be enforced by use of InOrder and/or Call.After. +// Call.After can create more varied call order dependencies, but InOrder is +// often more convenient. +// +// The following examples create equivalent call order dependencies. +// +// Example of using Call.After to chain expected call order: +// +// firstCall := mockObj.EXPECT().SomeMethod(1, "first") +// secondCall := mockObj.EXPECT().SomeMethod(2, "second").After(firstCall) +// mockObj.EXPECT().SomeMethod(3, "third").After(secondCall) +// +// Example of using InOrder to declare expected call order: +// +// gomock.InOrder( +// mockObj.EXPECT().SomeMethod(1, "first"), +// mockObj.EXPECT().SomeMethod(2, "second"), +// mockObj.EXPECT().SomeMethod(3, "third"), +// ) +// +// The standard TestReporter most users will pass to `NewController` is a +// `*testing.T` from the context of the test. Note that this will use the +// standard `t.Error` and `t.Fatal` methods to report what happened in the test. +// In some cases this can leave your testing package in a weird state if global +// state is used since `t.Fatal` is like calling panic in the middle of a +// function. In these cases it is recommended that you pass in your own +// `TestReporter`. +package gomock diff --git a/vendor/go.uber.org/mock/gomock/matchers.go b/vendor/go.uber.org/mock/gomock/matchers.go new file mode 100644 index 000000000..d52495f39 --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/matchers.go @@ -0,0 +1,447 @@ +// Copyright 2010 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gomock + +import ( + "fmt" + "reflect" + "regexp" + "strings" +) + +// A Matcher is a representation of a class of values. +// It is used to represent the valid or expected arguments to a mocked method. +type Matcher interface { + // Matches returns whether x is a match. + Matches(x any) bool + + // String describes what the matcher matches. + String() string +} + +// WantFormatter modifies the given Matcher's String() method to the given +// Stringer. This allows for control on how the "Want" is formatted when +// printing . +func WantFormatter(s fmt.Stringer, m Matcher) Matcher { + type matcher interface { + Matches(x any) bool + } + + return struct { + matcher + fmt.Stringer + }{ + matcher: m, + Stringer: s, + } +} + +// StringerFunc type is an adapter to allow the use of ordinary functions as +// a Stringer. If f is a function with the appropriate signature, +// StringerFunc(f) is a Stringer that calls f. +type StringerFunc func() string + +// String implements fmt.Stringer. +func (f StringerFunc) String() string { + return f() +} + +// GotFormatter is used to better print failure messages. If a matcher +// implements GotFormatter, it will use the result from Got when printing +// the failure message. +type GotFormatter interface { + // Got is invoked with the received value. The result is used when + // printing the failure message. + Got(got any) string +} + +// GotFormatterFunc type is an adapter to allow the use of ordinary +// functions as a GotFormatter. If f is a function with the appropriate +// signature, GotFormatterFunc(f) is a GotFormatter that calls f. +type GotFormatterFunc func(got any) string + +// Got implements GotFormatter. +func (f GotFormatterFunc) Got(got any) string { + return f(got) +} + +// GotFormatterAdapter attaches a GotFormatter to a Matcher. +func GotFormatterAdapter(s GotFormatter, m Matcher) Matcher { + return struct { + GotFormatter + Matcher + }{ + GotFormatter: s, + Matcher: m, + } +} + +type anyMatcher struct{} + +func (anyMatcher) Matches(any) bool { + return true +} + +func (anyMatcher) String() string { + return "is anything" +} + +type condMatcher[T any] struct { + fn func(x T) bool +} + +func (c condMatcher[T]) Matches(x any) bool { + typed, ok := x.(T) + if !ok { + return false + } + return c.fn(typed) +} + +func (c condMatcher[T]) String() string { + return "adheres to a custom condition" +} + +type eqMatcher struct { + x any +} + +func (e eqMatcher) Matches(x any) bool { + // In case, some value is nil + if e.x == nil || x == nil { + return reflect.DeepEqual(e.x, x) + } + + // Check if types assignable and convert them to common type + x1Val := reflect.ValueOf(e.x) + x2Val := reflect.ValueOf(x) + + if x1Val.Type().AssignableTo(x2Val.Type()) { + x1ValConverted := x1Val.Convert(x2Val.Type()) + return reflect.DeepEqual(x1ValConverted.Interface(), x2Val.Interface()) + } + + return false +} + +func (e eqMatcher) String() string { + return fmt.Sprintf("is equal to %s (%T)", getString(e.x), e.x) +} + +type nilMatcher struct{} + +func (nilMatcher) Matches(x any) bool { + if x == nil { + return true + } + + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice: + return v.IsNil() + } + + return false +} + +func (nilMatcher) String() string { + return "is nil" +} + +type notMatcher struct { + m Matcher +} + +func (n notMatcher) Matches(x any) bool { + return !n.m.Matches(x) +} + +func (n notMatcher) String() string { + return "not(" + n.m.String() + ")" +} + +type regexMatcher struct { + regex *regexp.Regexp +} + +func (m regexMatcher) Matches(x any) bool { + switch t := x.(type) { + case string: + return m.regex.MatchString(t) + case []byte: + return m.regex.Match(t) + default: + return false + } +} + +func (m regexMatcher) String() string { + return "matches regex " + m.regex.String() +} + +type assignableToTypeOfMatcher struct { + targetType reflect.Type +} + +func (m assignableToTypeOfMatcher) Matches(x any) bool { + return reflect.TypeOf(x).AssignableTo(m.targetType) +} + +func (m assignableToTypeOfMatcher) String() string { + return "is assignable to " + m.targetType.Name() +} + +type anyOfMatcher struct { + matchers []Matcher +} + +func (am anyOfMatcher) Matches(x any) bool { + for _, m := range am.matchers { + if m.Matches(x) { + return true + } + } + return false +} + +func (am anyOfMatcher) String() string { + ss := make([]string, 0, len(am.matchers)) + for _, matcher := range am.matchers { + ss = append(ss, matcher.String()) + } + return strings.Join(ss, " | ") +} + +type allMatcher struct { + matchers []Matcher +} + +func (am allMatcher) Matches(x any) bool { + for _, m := range am.matchers { + if !m.Matches(x) { + return false + } + } + return true +} + +func (am allMatcher) String() string { + ss := make([]string, 0, len(am.matchers)) + for _, matcher := range am.matchers { + ss = append(ss, matcher.String()) + } + return strings.Join(ss, "; ") +} + +type lenMatcher struct { + i int +} + +func (m lenMatcher) Matches(x any) bool { + v := reflect.ValueOf(x) + switch v.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == m.i + default: + return false + } +} + +func (m lenMatcher) String() string { + return fmt.Sprintf("has length %d", m.i) +} + +type inAnyOrderMatcher struct { + x any +} + +func (m inAnyOrderMatcher) Matches(x any) bool { + given, ok := m.prepareValue(x) + if !ok { + return false + } + wanted, ok := m.prepareValue(m.x) + if !ok { + return false + } + + if given.Len() != wanted.Len() { + return false + } + + usedFromGiven := make([]bool, given.Len()) + foundFromWanted := make([]bool, wanted.Len()) + for i := 0; i < wanted.Len(); i++ { + wantedMatcher := Eq(wanted.Index(i).Interface()) + for j := 0; j < given.Len(); j++ { + if usedFromGiven[j] { + continue + } + if wantedMatcher.Matches(given.Index(j).Interface()) { + foundFromWanted[i] = true + usedFromGiven[j] = true + break + } + } + } + + missingFromWanted := 0 + for _, found := range foundFromWanted { + if !found { + missingFromWanted++ + } + } + extraInGiven := 0 + for _, used := range usedFromGiven { + if !used { + extraInGiven++ + } + } + + return extraInGiven == 0 && missingFromWanted == 0 +} + +func (m inAnyOrderMatcher) prepareValue(x any) (reflect.Value, bool) { + xValue := reflect.ValueOf(x) + switch xValue.Kind() { + case reflect.Slice, reflect.Array: + return xValue, true + default: + return reflect.Value{}, false + } +} + +func (m inAnyOrderMatcher) String() string { + return fmt.Sprintf("has the same elements as %v", m.x) +} + +// Constructors + +// All returns a composite Matcher that returns true if and only all of the +// matchers return true. +func All(ms ...Matcher) Matcher { return allMatcher{ms} } + +// Any returns a matcher that always matches. +func Any() Matcher { return anyMatcher{} } + +// Cond returns a matcher that matches when the given function returns true +// after passing it the parameter to the mock function. +// This is particularly useful in case you want to match over a field of a custom struct, or dynamic logic. +// +// Example usage: +// +// Cond(func(x int){return x == 1}).Matches(1) // returns true +// Cond(func(x int){return x == 2}).Matches(1) // returns false +func Cond[T any](fn func(x T) bool) Matcher { return condMatcher[T]{fn} } + +// AnyOf returns a composite Matcher that returns true if at least one of the +// matchers returns true. +// +// Example usage: +// +// AnyOf(1, 2, 3).Matches(2) // returns true +// AnyOf(1, 2, 3).Matches(10) // returns false +// AnyOf(Nil(), Len(2)).Matches(nil) // returns true +// AnyOf(Nil(), Len(2)).Matches("hi") // returns true +// AnyOf(Nil(), Len(2)).Matches("hello") // returns false +func AnyOf(xs ...any) Matcher { + ms := make([]Matcher, 0, len(xs)) + for _, x := range xs { + if m, ok := x.(Matcher); ok { + ms = append(ms, m) + } else { + ms = append(ms, Eq(x)) + } + } + return anyOfMatcher{ms} +} + +// Eq returns a matcher that matches on equality. +// +// Example usage: +// +// Eq(5).Matches(5) // returns true +// Eq(5).Matches(4) // returns false +func Eq(x any) Matcher { return eqMatcher{x} } + +// Len returns a matcher that matches on length. This matcher returns false if +// is compared to a type that is not an array, chan, map, slice, or string. +func Len(i int) Matcher { + return lenMatcher{i} +} + +// Nil returns a matcher that matches if the received value is nil. +// +// Example usage: +// +// var x *bytes.Buffer +// Nil().Matches(x) // returns true +// x = &bytes.Buffer{} +// Nil().Matches(x) // returns false +func Nil() Matcher { return nilMatcher{} } + +// Not reverses the results of its given child matcher. +// +// Example usage: +// +// Not(Eq(5)).Matches(4) // returns true +// Not(Eq(5)).Matches(5) // returns false +func Not(x any) Matcher { + if m, ok := x.(Matcher); ok { + return notMatcher{m} + } + return notMatcher{Eq(x)} +} + +// Regex checks whether parameter matches the associated regex. +// +// Example usage: +// +// Regex("[0-9]{2}:[0-9]{2}").Matches("23:02") // returns true +// Regex("[0-9]{2}:[0-9]{2}").Matches([]byte{'2', '3', ':', '0', '2'}) // returns true +// Regex("[0-9]{2}:[0-9]{2}").Matches("hello world") // returns false +// Regex("[0-9]{2}").Matches(21) // returns false as it's not a valid type +func Regex(regexStr string) Matcher { + return regexMatcher{regex: regexp.MustCompile(regexStr)} +} + +// AssignableToTypeOf is a Matcher that matches if the parameter to the mock +// function is assignable to the type of the parameter to this function. +// +// Example usage: +// +// var s fmt.Stringer = &bytes.Buffer{} +// AssignableToTypeOf(s).Matches(time.Second) // returns true +// AssignableToTypeOf(s).Matches(99) // returns false +// +// var ctx = reflect.TypeOf((*context.Context)(nil)).Elem() +// AssignableToTypeOf(ctx).Matches(context.Background()) // returns true +func AssignableToTypeOf(x any) Matcher { + if xt, ok := x.(reflect.Type); ok { + return assignableToTypeOfMatcher{xt} + } + return assignableToTypeOfMatcher{reflect.TypeOf(x)} +} + +// InAnyOrder is a Matcher that returns true for collections of the same elements ignoring the order. +// +// Example usage: +// +// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 3, 2}) // returns true +// InAnyOrder([]int{1, 2, 3}).Matches([]int{1, 2}) // returns false +func InAnyOrder(x any) Matcher { + return inAnyOrderMatcher{x} +} diff --git a/vendor/go.uber.org/mock/gomock/string.go b/vendor/go.uber.org/mock/gomock/string.go new file mode 100644 index 000000000..ec4ca7e4d --- /dev/null +++ b/vendor/go.uber.org/mock/gomock/string.go @@ -0,0 +1,36 @@ +package gomock + +import ( + "fmt" + "reflect" +) + +// getString is a safe way to convert a value to a string for printing results +// If the value is a a mock, getString avoids calling the mocked String() method, +// which avoids potential deadlocks +func getString(x any) string { + if isGeneratedMock(x) { + return fmt.Sprintf("%T", x) + } + if s, ok := x.(fmt.Stringer); ok { + return s.String() + } + return fmt.Sprintf("%v", x) +} + +// isGeneratedMock checks if the given type has a "isgomock" field, +// indicating it is a generated mock. +func isGeneratedMock(x any) bool { + typ := reflect.TypeOf(x) + if typ == nil { + return false + } + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + if typ.Kind() != reflect.Struct { + return false + } + _, isgomock := typ.FieldByName("isgomock") + return isgomock +} diff --git a/vendor/go.uber.org/mock/mockgen/model/model.go b/vendor/go.uber.org/mock/mockgen/model/model.go new file mode 100644 index 000000000..853dbf2d6 --- /dev/null +++ b/vendor/go.uber.org/mock/mockgen/model/model.go @@ -0,0 +1,533 @@ +// Copyright 2012 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package model contains the data model necessary for generating mock implementations. +package model + +import ( + "encoding/gob" + "fmt" + "io" + "reflect" + "strings" +) + +// pkgPath is the importable path for package model +const pkgPath = "go.uber.org/mock/mockgen/model" + +// Package is a Go package. It may be a subset. +type Package struct { + Name string + PkgPath string + Interfaces []*Interface + DotImports []string +} + +// Print writes the package name and its exported interfaces. +func (pkg *Package) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, "package %s\n", pkg.Name) + for _, intf := range pkg.Interfaces { + intf.Print(w) + } +} + +// Imports returns the imports needed by the Package as a set of import paths. +func (pkg *Package) Imports() map[string]bool { + im := make(map[string]bool) + for _, intf := range pkg.Interfaces { + intf.addImports(im) + for _, tp := range intf.TypeParams { + tp.Type.addImports(im) + } + } + return im +} + +// Interface is a Go interface. +type Interface struct { + Name string + Methods []*Method + TypeParams []*Parameter +} + +// Print writes the interface name and its methods. +func (intf *Interface) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, "interface %s\n", intf.Name) + for _, m := range intf.Methods { + m.Print(w) + } +} + +func (intf *Interface) addImports(im map[string]bool) { + for _, m := range intf.Methods { + m.addImports(im) + } +} + +// AddMethod adds a new method, de-duplicating by method name. +func (intf *Interface) AddMethod(m *Method) { + for _, me := range intf.Methods { + if me.Name == m.Name { + return + } + } + intf.Methods = append(intf.Methods, m) +} + +// Method is a single method of an interface. +type Method struct { + Name string + In, Out []*Parameter + Variadic *Parameter // may be nil +} + +// Print writes the method name and its signature. +func (m *Method) Print(w io.Writer) { + _, _ = fmt.Fprintf(w, " - method %s\n", m.Name) + if len(m.In) > 0 { + _, _ = fmt.Fprintf(w, " in:\n") + for _, p := range m.In { + p.Print(w) + } + } + if m.Variadic != nil { + _, _ = fmt.Fprintf(w, " ...:\n") + m.Variadic.Print(w) + } + if len(m.Out) > 0 { + _, _ = fmt.Fprintf(w, " out:\n") + for _, p := range m.Out { + p.Print(w) + } + } +} + +func (m *Method) addImports(im map[string]bool) { + for _, p := range m.In { + p.Type.addImports(im) + } + if m.Variadic != nil { + m.Variadic.Type.addImports(im) + } + for _, p := range m.Out { + p.Type.addImports(im) + } +} + +// Parameter is an argument or return parameter of a method. +type Parameter struct { + Name string // may be empty + Type Type +} + +// Print writes a method parameter. +func (p *Parameter) Print(w io.Writer) { + n := p.Name + if n == "" { + n = `""` + } + _, _ = fmt.Fprintf(w, " - %v: %v\n", n, p.Type.String(nil, "")) +} + +// Type is a Go type. +type Type interface { + String(pm map[string]string, pkgOverride string) string + addImports(im map[string]bool) +} + +func init() { + // Call gob.RegisterName with pkgPath as prefix to avoid conflicting with + // github.com/golang/mock/mockgen/model 's registration. + gob.RegisterName(pkgPath+".ArrayType", &ArrayType{}) + gob.RegisterName(pkgPath+".ChanType", &ChanType{}) + gob.RegisterName(pkgPath+".FuncType", &FuncType{}) + gob.RegisterName(pkgPath+".MapType", &MapType{}) + gob.RegisterName(pkgPath+".NamedType", &NamedType{}) + gob.RegisterName(pkgPath+".PointerType", &PointerType{}) + + // Call gob.RegisterName to make sure it has the consistent name registered + // for both gob decoder and encoder. + // + // For a non-pointer type, gob.Register will try to get package full path by + // calling rt.PkgPath() for a name to register. If your project has vendor + // directory, it is possible that PkgPath will get a path like this: + // ../../../vendor/go.uber.org/mock/mockgen/model + gob.RegisterName(pkgPath+".PredeclaredType", PredeclaredType("")) +} + +// ArrayType is an array or slice type. +type ArrayType struct { + Len int // -1 for slices, >= 0 for arrays + Type Type +} + +func (at *ArrayType) String(pm map[string]string, pkgOverride string) string { + s := "[]" + if at.Len > -1 { + s = fmt.Sprintf("[%d]", at.Len) + } + return s + at.Type.String(pm, pkgOverride) +} + +func (at *ArrayType) addImports(im map[string]bool) { at.Type.addImports(im) } + +// ChanType is a channel type. +type ChanType struct { + Dir ChanDir // 0, 1 or 2 + Type Type +} + +func (ct *ChanType) String(pm map[string]string, pkgOverride string) string { + s := ct.Type.String(pm, pkgOverride) + if ct.Dir == RecvDir { + return "<-chan " + s + } + if ct.Dir == SendDir { + return "chan<- " + s + } + return "chan " + s +} + +func (ct *ChanType) addImports(im map[string]bool) { ct.Type.addImports(im) } + +// ChanDir is a channel direction. +type ChanDir int + +// Constants for channel directions. +const ( + RecvDir ChanDir = 1 + SendDir ChanDir = 2 +) + +// FuncType is a function type. +type FuncType struct { + In, Out []*Parameter + Variadic *Parameter // may be nil +} + +func (ft *FuncType) String(pm map[string]string, pkgOverride string) string { + args := make([]string, len(ft.In)) + for i, p := range ft.In { + args[i] = p.Type.String(pm, pkgOverride) + } + if ft.Variadic != nil { + args = append(args, "..."+ft.Variadic.Type.String(pm, pkgOverride)) + } + rets := make([]string, len(ft.Out)) + for i, p := range ft.Out { + rets[i] = p.Type.String(pm, pkgOverride) + } + retString := strings.Join(rets, ", ") + if nOut := len(ft.Out); nOut == 1 { + retString = " " + retString + } else if nOut > 1 { + retString = " (" + retString + ")" + } + return "func(" + strings.Join(args, ", ") + ")" + retString +} + +func (ft *FuncType) addImports(im map[string]bool) { + for _, p := range ft.In { + p.Type.addImports(im) + } + if ft.Variadic != nil { + ft.Variadic.Type.addImports(im) + } + for _, p := range ft.Out { + p.Type.addImports(im) + } +} + +// MapType is a map type. +type MapType struct { + Key, Value Type +} + +func (mt *MapType) String(pm map[string]string, pkgOverride string) string { + return "map[" + mt.Key.String(pm, pkgOverride) + "]" + mt.Value.String(pm, pkgOverride) +} + +func (mt *MapType) addImports(im map[string]bool) { + mt.Key.addImports(im) + mt.Value.addImports(im) +} + +// NamedType is an exported type in a package. +type NamedType struct { + Package string // may be empty + Type string + TypeParams *TypeParametersType +} + +func (nt *NamedType) String(pm map[string]string, pkgOverride string) string { + if pkgOverride == nt.Package { + return nt.Type + nt.TypeParams.String(pm, pkgOverride) + } + prefix := pm[nt.Package] + if prefix != "" { + return prefix + "." + nt.Type + nt.TypeParams.String(pm, pkgOverride) + } + + return nt.Type + nt.TypeParams.String(pm, pkgOverride) +} + +func (nt *NamedType) addImports(im map[string]bool) { + if nt.Package != "" { + im[nt.Package] = true + } + nt.TypeParams.addImports(im) +} + +// PointerType is a pointer to another type. +type PointerType struct { + Type Type +} + +func (pt *PointerType) String(pm map[string]string, pkgOverride string) string { + return "*" + pt.Type.String(pm, pkgOverride) +} +func (pt *PointerType) addImports(im map[string]bool) { pt.Type.addImports(im) } + +// PredeclaredType is a predeclared type such as "int". +type PredeclaredType string + +func (pt PredeclaredType) String(map[string]string, string) string { return string(pt) } +func (pt PredeclaredType) addImports(map[string]bool) {} + +// TypeParametersType contains type parameters for a NamedType. +type TypeParametersType struct { + TypeParameters []Type +} + +func (tp *TypeParametersType) String(pm map[string]string, pkgOverride string) string { + if tp == nil || len(tp.TypeParameters) == 0 { + return "" + } + var sb strings.Builder + sb.WriteString("[") + for i, v := range tp.TypeParameters { + if i != 0 { + sb.WriteString(", ") + } + sb.WriteString(v.String(pm, pkgOverride)) + } + sb.WriteString("]") + return sb.String() +} + +func (tp *TypeParametersType) addImports(im map[string]bool) { + if tp == nil { + return + } + for _, v := range tp.TypeParameters { + v.addImports(im) + } +} + +// The following code is intended to be called by the program generated by ../reflect.go. + +// InterfaceFromInterfaceType returns a pointer to an interface for the +// given reflection interface type. +func InterfaceFromInterfaceType(it reflect.Type) (*Interface, error) { + if it.Kind() != reflect.Interface { + return nil, fmt.Errorf("%v is not an interface", it) + } + intf := &Interface{} + + for i := 0; i < it.NumMethod(); i++ { + mt := it.Method(i) + // TODO: need to skip unexported methods? or just raise an error? + m := &Method{ + Name: mt.Name, + } + + var err error + m.In, m.Variadic, m.Out, err = funcArgsFromType(mt.Type) + if err != nil { + return nil, err + } + + intf.AddMethod(m) + } + + return intf, nil +} + +// t's Kind must be a reflect.Func. +func funcArgsFromType(t reflect.Type) (in []*Parameter, variadic *Parameter, out []*Parameter, err error) { + nin := t.NumIn() + if t.IsVariadic() { + nin-- + } + var p *Parameter + for i := 0; i < nin; i++ { + p, err = parameterFromType(t.In(i)) + if err != nil { + return + } + in = append(in, p) + } + if t.IsVariadic() { + p, err = parameterFromType(t.In(nin).Elem()) + if err != nil { + return + } + variadic = p + } + for i := 0; i < t.NumOut(); i++ { + p, err = parameterFromType(t.Out(i)) + if err != nil { + return + } + out = append(out, p) + } + return +} + +func parameterFromType(t reflect.Type) (*Parameter, error) { + tt, err := typeFromType(t) + if err != nil { + return nil, err + } + return &Parameter{Type: tt}, nil +} + +var errorType = reflect.TypeOf((*error)(nil)).Elem() + +var byteType = reflect.TypeOf(byte(0)) + +func typeFromType(t reflect.Type) (Type, error) { + // Hack workaround for https://golang.org/issue/3853. + // This explicit check should not be necessary. + if t == byteType { + return PredeclaredType("byte"), nil + } + + if imp := t.PkgPath(); imp != "" { + return &NamedType{ + Package: impPath(imp), + Type: t.Name(), + }, nil + } + + // only unnamed or predeclared types after here + + // Lots of types have element types. Let's do the parsing and error checking for all of them. + var elemType Type + switch t.Kind() { + case reflect.Array, reflect.Chan, reflect.Map, reflect.Ptr, reflect.Slice: + var err error + elemType, err = typeFromType(t.Elem()) + if err != nil { + return nil, err + } + } + + switch t.Kind() { + case reflect.Array: + return &ArrayType{ + Len: t.Len(), + Type: elemType, + }, nil + case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, + reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128, reflect.String: + return PredeclaredType(t.Kind().String()), nil + case reflect.Chan: + var dir ChanDir + switch t.ChanDir() { + case reflect.RecvDir: + dir = RecvDir + case reflect.SendDir: + dir = SendDir + } + return &ChanType{ + Dir: dir, + Type: elemType, + }, nil + case reflect.Func: + in, variadic, out, err := funcArgsFromType(t) + if err != nil { + return nil, err + } + return &FuncType{ + In: in, + Out: out, + Variadic: variadic, + }, nil + case reflect.Interface: + // Two special interfaces. + if t.NumMethod() == 0 { + return PredeclaredType("any"), nil + } + if t == errorType { + return PredeclaredType("error"), nil + } + case reflect.Map: + kt, err := typeFromType(t.Key()) + if err != nil { + return nil, err + } + return &MapType{ + Key: kt, + Value: elemType, + }, nil + case reflect.Ptr: + return &PointerType{ + Type: elemType, + }, nil + case reflect.Slice: + return &ArrayType{ + Len: -1, + Type: elemType, + }, nil + case reflect.Struct: + if t.NumField() == 0 { + return PredeclaredType("struct{}"), nil + } + } + + // TODO: Struct, UnsafePointer + return nil, fmt.Errorf("can't yet turn %v (%v) into a model.Type", t, t.Kind()) +} + +// impPath sanitizes the package path returned by `PkgPath` method of a reflect Type so that +// it is importable. PkgPath might return a path that includes "vendor". These paths do not +// compile, so we need to remove everything up to and including "/vendor/". +// See https://github.com/golang/go/issues/12019. +func impPath(imp string) string { + if strings.HasPrefix(imp, "vendor/") { + imp = "/" + imp + } + if i := strings.LastIndex(imp, "/vendor/"); i != -1 { + imp = imp[i+len("/vendor/"):] + } + return imp +} + +// ErrorInterface represent built-in error interface. +var ErrorInterface = Interface{ + Name: "error", + Methods: []*Method{ + { + Name: "Error", + Out: []*Parameter{ + { + Name: "", + Type: PredeclaredType("string"), + }, + }, + }, + }, +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 482041da7..45213ba76 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -477,6 +477,9 @@ github.com/gobwas/glob/util/strings # github.com/gofrs/flock v0.12.1 ## explicit; go 1.21.0 github.com/gofrs/flock +# github.com/gofrs/uuid/v5 v5.3.0 +## explicit; go 1.19 +github.com/gofrs/uuid/v5 # github.com/gogo/protobuf v1.3.2 ## explicit; go 1.15 github.com/gogo/protobuf/proto @@ -718,9 +721,52 @@ github.com/google/shlex # github.com/google/uuid v1.6.0 ## explicit github.com/google/uuid -# github.com/gophercloud/gophercloud/v2 v2.7.0 +# github.com/gophercloud/gophercloud/v2 v2.9.0 ## explicit; go 1.22 github.com/gophercloud/gophercloud/v2 +github.com/gophercloud/gophercloud/v2/openstack +github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes +github.com/gophercloud/gophercloud/v2/openstack/common/extensions +github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces +github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones +github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors +github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups +github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers +github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tenants +github.com/gophercloud/gophercloud/v2/openstack/identity/v2/tokens +github.com/gophercloud/gophercloud/v2/openstack/identity/v3/ec2tokens +github.com/gophercloud/gophercloud/v2/openstack/identity/v3/oauth1 +github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens +github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata +github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport +github.com/gophercloud/gophercloud/v2/openstack/image/v2/images +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/l7policies +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools +github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports +github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets +github.com/gophercloud/gophercloud/v2/openstack/utils +github.com/gophercloud/gophercloud/v2/pagination +# github.com/gophercloud/utils/v2 v2.0.0-20241209100706-e3a3b7c07d26 +## explicit; go 1.22 +github.com/gophercloud/utils/v2/client +github.com/gophercloud/utils/v2/env +github.com/gophercloud/utils/v2/gnocchi +github.com/gophercloud/utils/v2/internal +github.com/gophercloud/utils/v2/openstack/clientconfig # github.com/gordonklaus/ineffassign v0.1.0 ## explicit; go 1.14 github.com/gordonklaus/ineffassign/pkg/ineffassign @@ -1480,6 +1526,10 @@ go.uber.org/automaxprocs go.uber.org/automaxprocs/internal/cgroups go.uber.org/automaxprocs/internal/runtime go.uber.org/automaxprocs/maxprocs +# go.uber.org/mock v0.5.2 +## explicit; go 1.23 +go.uber.org/mock/gomock +go.uber.org/mock/mockgen/model # go.uber.org/multierr v1.11.0 ## explicit; go 1.19 go.uber.org/multierr @@ -2705,8 +2755,14 @@ sigs.k8s.io/cluster-api-provider-ibmcloud/api/v1beta2 # sigs.k8s.io/cluster-api-provider-openstack v0.12.4 ## explicit; go 1.23.0 sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1 +sigs.k8s.io/cluster-api-provider-openstack/pkg/clients +sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock +sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics +sigs.k8s.io/cluster-api-provider-openstack/pkg/scope sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors +sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/optional +sigs.k8s.io/cluster-api-provider-openstack/version # sigs.k8s.io/cluster-api-provider-vsphere v1.13.0 ## explicit; go 1.23.0 sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1 diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/compute.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/compute.go new file mode 100644 index 000000000..0338417b9 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/compute.go @@ -0,0 +1,259 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups" + "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" + openstackutil "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack" +) + +/* +Constants for specific microversion requirements. +2.60 corresponds to OpenStack Queens and 2.53 to OpenStack Pike, +2.38 is the maximum in OpenStack Newton. + +For the canonical description of Nova microversions, see +https://docs.openstack.org/nova/latest/reference/api-microversion-history.html + +CAPO uses server tags, which were first added in microversion 2.26 and then refined +in 2.52 so it is possible to apply them when creating a server (which is what CAPO does). +We round up to 2.53 here since that takes us to the maximum in Pike. + +CAPO supports multiattach volume types, which were added in microversion 2.60. + +2.38 was chosen as a base level since it is reasonably old, but not too old. +*/ +const ( + MinimumNovaMicroversion = "2.38" + NovaTagging = "2.53" + NovaMultiAttachVolume = "2.60" +) + +type ComputeClient interface { + ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) + + ListFlavors() ([]flavors.Flavor, error) + CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) + DeleteServer(serverID string) error + GetServer(serverID string) (*servers.Server, error) + ListServers(listOpts servers.ListOptsBuilder) ([]servers.Server, error) + + ListAttachedInterfaces(serverID string) ([]attachinterfaces.Interface, error) + DeleteAttachedInterface(serverID, portID string) error + + ListServerGroups() ([]servergroups.ServerGroup, error) + GetConsoleOutput(serverID string) (string, error) + WithMicroversion(required string) (ComputeClient, error) +} + +type computeClient struct { + client *gophercloud.ServiceClient + minVersion string + maxVersion string +} + +// NewComputeClient returns a new compute client. +func NewComputeClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (ComputeClient, error) { + compute, err := openstack.NewComputeV2(providerClient, gophercloud.EndpointOpts{ + Region: providerClientOpts.RegionName, + Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType), + }) + if err != nil { + return nil, fmt.Errorf("failed to create compute service client: %v", err) + } + + // Find the minimum and maximum versions supported by the server + serviceMin, serviceMax, err := openstackutil.GetSupportedMicroversions(*compute) + if err != nil { + return nil, fmt.Errorf("unable to verify compatible server version: %w", err) + } + + supported, err := openstackutil.MicroversionSupported(MinimumNovaMicroversion, serviceMin, serviceMax) + if err != nil { + return nil, fmt.Errorf("unable to verify compatible server version: %w", err) + } + if !supported { + return nil, fmt.Errorf("no compatible server version. CAPO requires %s, but min=%s and max=%s", + MinimumNovaMicroversion, serviceMin, serviceMax) + } + + compute.Microversion = MinimumNovaMicroversion + + return &computeClient{client: compute, minVersion: serviceMin, maxVersion: serviceMax}, nil +} + +func (c computeClient) ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) { + mc := metrics.NewMetricPrometheusContext("availability_zone", "list") + allPages, err := availabilityzones.List(c.client).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return availabilityzones.ExtractAvailabilityZones(allPages) +} + +func (c computeClient) ListFlavors() ([]flavors.Flavor, error) { + mc := metrics.NewMetricPrometheusContext("flavor", "list") + allPages, err := flavors.ListDetail(c.client, &flavors.ListOpts{}).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return flavors.ExtractFlavors(allPages) +} + +func (c computeClient) CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) { + mc := metrics.NewMetricPrometheusContext("server", "create") + server, err := servers.Create(context.TODO(), c.client, createOpts, schedulerHints).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return server, nil +} + +func (c computeClient) DeleteServer(serverID string) error { + mc := metrics.NewMetricPrometheusContext("server", "delete") + err := servers.Delete(context.TODO(), c.client, serverID).ExtractErr() + return mc.ObserveRequestIgnoreNotFound(err) +} + +func (c computeClient) GetServer(serverID string) (*servers.Server, error) { + var server servers.Server + mc := metrics.NewMetricPrometheusContext("server", "get") + err := servers.Get(context.TODO(), c.client, serverID).ExtractInto(&server) + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return &server, nil +} + +func (c computeClient) ListServers(listOpts servers.ListOptsBuilder) ([]servers.Server, error) { + var serverList []servers.Server + mc := metrics.NewMetricPrometheusContext("server", "list") + allPages, err := servers.List(c.client, listOpts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + err = servers.ExtractServersInto(allPages, &serverList) + return serverList, err +} + +func (c computeClient) ListAttachedInterfaces(serverID string) ([]attachinterfaces.Interface, error) { + mc := metrics.NewMetricPrometheusContext("server_os_interface", "list") + interfaces, err := attachinterfaces.List(c.client, serverID).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return attachinterfaces.ExtractInterfaces(interfaces) +} + +func (c computeClient) DeleteAttachedInterface(serverID, portID string) error { + mc := metrics.NewMetricPrometheusContext("server_os_interface", "delete") + err := attachinterfaces.Delete(context.TODO(), c.client, serverID, portID).ExtractErr() + return mc.ObserveRequestIgnoreNotFoundorConflict(err) +} + +func (c computeClient) ListServerGroups() ([]servergroups.ServerGroup, error) { + mc := metrics.NewMetricPrometheusContext("server_group", "list") + opts := servergroups.ListOpts{} + allPages, err := servergroups.List(c.client, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return servergroups.ExtractServerGroups(allPages) +} + +func (c computeClient) GetConsoleOutput(serverID string) (string, error) { + opts := servers.ShowConsoleOutputOpts{} + return servers.ShowConsoleOutput(context.TODO(), c.client, serverID, opts).Extract() +} + +// WithMicroversion checks that the required Nova microversion is supported and sets it for +// the ComputeClient. +func (c computeClient) WithMicroversion(required string) (ComputeClient, error) { + supported, err := openstackutil.MicroversionSupported(required, c.minVersion, c.maxVersion) + if err != nil { + return nil, err + } + if !supported { + return nil, fmt.Errorf("microversion %s not supported. Min=%s, max=%s", required, c.minVersion, c.maxVersion) + } + versionedClient := c + versionedClient.client.Microversion = required + return versionedClient, nil +} + +type computeErrorClient struct{ error } + +// NewComputeErrorClient returns a ComputeClient in which every method returns the given error. +func NewComputeErrorClient(e error) ComputeClient { + return computeErrorClient{e} +} + +func (e computeErrorClient) ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) { + return nil, e.error +} + +func (e computeErrorClient) ListFlavors() ([]flavors.Flavor, error) { + return nil, e.error +} + +func (e computeErrorClient) CreateServer(_ servers.CreateOptsBuilder, _ servers.SchedulerHintOptsBuilder) (*servers.Server, error) { + return nil, e.error +} + +func (e computeErrorClient) DeleteServer(_ string) error { + return e.error +} + +func (e computeErrorClient) GetServer(_ string) (*servers.Server, error) { + return nil, e.error +} + +func (e computeErrorClient) ListServers(_ servers.ListOptsBuilder) ([]servers.Server, error) { + return nil, e.error +} + +func (e computeErrorClient) ListAttachedInterfaces(_ string) ([]attachinterfaces.Interface, error) { + return nil, e.error +} + +func (e computeErrorClient) DeleteAttachedInterface(_, _ string) error { + return e.error +} + +func (e computeErrorClient) ListServerGroups() ([]servergroups.ServerGroup, error) { + return nil, e.error +} + +func (e computeErrorClient) GetConsoleOutput(_ string) (string, error) { + return "", e.error +} + +func (e computeErrorClient) WithMicroversion(_ string) (ComputeClient, error) { + return nil, e.error +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/image.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/image.go new file mode 100644 index 000000000..b43996e63 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/image.go @@ -0,0 +1,147 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + "fmt" + "io" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/imagedata" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport" + "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" +) + +type ImageClient interface { + ListImages(listOpts images.ListOptsBuilder) ([]images.Image, error) + GetImage(id string) (*images.Image, error) + CreateImage(ctx context.Context, createOpts images.CreateOptsBuilder) (*images.Image, error) + DeleteImage(ctx context.Context, id string) error + UploadData(ctx context.Context, id string, data io.Reader) error + GetImportInfo(ctx context.Context) (*imageimport.ImportInfo, error) + CreateImport(ctx context.Context, id string, createOpts imageimport.CreateOptsBuilder) error +} + +type imageClient struct{ client *gophercloud.ServiceClient } + +// NewImageClient returns a new glance client. +func NewImageClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (ImageClient, error) { + images, err := openstack.NewImageV2(providerClient, gophercloud.EndpointOpts{ + Region: providerClientOpts.RegionName, + Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType), + }) + if err != nil { + return nil, fmt.Errorf("failed to create image service client: %v", err) + } + + return imageClient{images}, nil +} + +func (c imageClient) ListImages(listOpts images.ListOptsBuilder) ([]images.Image, error) { + mc := metrics.NewMetricPrometheusContext("image", "list") + pages, err := images.List(c.client, listOpts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return images.ExtractImages(pages) +} + +func (c imageClient) GetImage(id string) (*images.Image, error) { + image := &images.Image{} + mc := metrics.NewMetricPrometheusContext("image", "get") + err := images.Get(context.TODO(), c.client, id).ExtractInto(image) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return image, nil +} + +func (c imageClient) CreateImage(ctx context.Context, createOpts images.CreateOptsBuilder) (*images.Image, error) { + mc := metrics.NewMetricPrometheusContext("image", "create") + image, err := images.Create(ctx, c.client, createOpts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return image, nil +} + +func (c imageClient) DeleteImage(ctx context.Context, id string) error { + mc := metrics.NewMetricPrometheusContext("image", "delete") + err := images.Delete(ctx, c.client, id).ExtractErr() + return mc.ObserveRequestIgnoreNotFound(err) +} + +func (c imageClient) UploadData(ctx context.Context, id string, data io.Reader) error { + mc := metrics.NewMetricPrometheusContext("image", "upload") + err := imagedata.Upload(ctx, c.client, id, data).ExtractErr() + return mc.ObserveRequest(err) +} + +func (c imageClient) GetImportInfo(ctx context.Context) (*imageimport.ImportInfo, error) { + mc := metrics.NewMetricPrometheusContext("image", "getimportmethods") + importInfo, err := imageimport.Get(ctx, c.client).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return importInfo, nil +} + +func (c imageClient) CreateImport(ctx context.Context, id string, createOpts imageimport.CreateOptsBuilder) error { + mc := metrics.NewMetricPrometheusContext("image", "createimport") + err := imageimport.Create(ctx, c.client, id, createOpts).ExtractErr() + return mc.ObserveRequest(err) +} + +type imageErrorClient struct{ error } + +// NewImageErrorClient returns an ImageClient in which every method returns the given error. +func NewImageErrorClient(e error) ImageClient { + return imageErrorClient{e} +} + +func (e imageErrorClient) ListImages(_ images.ListOptsBuilder) ([]images.Image, error) { + return nil, e.error +} + +func (e imageErrorClient) GetImage(_ string) (*images.Image, error) { + return nil, e.error +} + +func (e imageErrorClient) CreateImage(_ context.Context, _ images.CreateOptsBuilder) (*images.Image, error) { + return nil, e.error +} + +func (e imageErrorClient) DeleteImage(_ context.Context, _ string) error { + return e.error +} + +func (e imageErrorClient) UploadData(_ context.Context, _ string, _ io.Reader) error { + return e.error +} + +func (e imageErrorClient) GetImportInfo(_ context.Context) (*imageimport.ImportInfo, error) { + return nil, e.error +} + +func (e imageErrorClient) CreateImport(_ context.Context, _ string, _ imageimport.CreateOptsBuilder) error { + return e.error +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/loadbalancer.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/loadbalancer.go new file mode 100644 index 000000000..2fd019ea3 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/loadbalancer.go @@ -0,0 +1,282 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools" + "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" + capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors" +) + +type LbClient interface { + CreateLoadBalancer(opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) + ListLoadBalancers(opts loadbalancers.ListOptsBuilder) ([]loadbalancers.LoadBalancer, error) + GetLoadBalancer(id string) (*loadbalancers.LoadBalancer, error) + DeleteLoadBalancer(id string, opts loadbalancers.DeleteOptsBuilder) error + CreateListener(opts listeners.CreateOptsBuilder) (*listeners.Listener, error) + ListListeners(opts listeners.ListOptsBuilder) ([]listeners.Listener, error) + UpdateListener(id string, opts listeners.UpdateOpts) (*listeners.Listener, error) + GetListener(id string) (*listeners.Listener, error) + DeleteListener(id string) error + CreatePool(opts pools.CreateOptsBuilder) (*pools.Pool, error) + ListPools(opts pools.ListOptsBuilder) ([]pools.Pool, error) + GetPool(id string) (*pools.Pool, error) + DeletePool(id string) error + CreatePoolMember(poolID string, opts pools.CreateMemberOptsBuilder) (*pools.Member, error) + ListPoolMember(poolID string, opts pools.ListMembersOptsBuilder) ([]pools.Member, error) + DeletePoolMember(poolID string, lbMemberID string) error + CreateMonitor(opts monitors.CreateOptsBuilder) (*monitors.Monitor, error) + ListMonitors(opts monitors.ListOptsBuilder) ([]monitors.Monitor, error) + DeleteMonitor(id string) error + ListLoadBalancerProviders() ([]providers.Provider, error) + ListOctaviaVersions() ([]apiversions.APIVersion, error) + ListLoadBalancerFlavors() ([]flavors.Flavor, error) +} + +type lbClient struct { + serviceClient *gophercloud.ServiceClient +} + +// NewLbClient returns a new loadbalancer client. +func NewLbClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (LbClient, error) { + loadbalancerClient, err := openstack.NewLoadBalancerV2(providerClient, gophercloud.EndpointOpts{ + Region: providerClientOpts.RegionName, + Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType), + }) + if err != nil { + return nil, fmt.Errorf("failed to create load balancer service client: %v", err) + } + + return &lbClient{loadbalancerClient}, nil +} + +func (l lbClient) CreateLoadBalancer(opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "create") + lb, err := loadbalancers.Create(context.TODO(), l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return lb, nil +} + +func (l lbClient) ListLoadBalancers(opts loadbalancers.ListOptsBuilder) ([]loadbalancers.LoadBalancer, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "list") + allPages, err := loadbalancers.List(l.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return loadbalancers.ExtractLoadBalancers(allPages) +} + +func (l lbClient) GetLoadBalancer(id string) (*loadbalancers.LoadBalancer, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "get") + lb, err := loadbalancers.Get(context.TODO(), l.serviceClient, id).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return lb, nil +} + +func (l lbClient) DeleteLoadBalancer(id string, opts loadbalancers.DeleteOptsBuilder) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer", "delete") + err := loadbalancers.Delete(context.TODO(), l.serviceClient, id, opts).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return err + } + return nil +} + +func (l lbClient) CreateListener(opts listeners.CreateOptsBuilder) (*listeners.Listener, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "create") + listener, err := listeners.Create(context.TODO(), l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return listener, nil +} + +func (l lbClient) UpdateListener(id string, opts listeners.UpdateOpts) (*listeners.Listener, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "update") + listener, err := listeners.Update(context.TODO(), l.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return listener, nil +} + +func (l lbClient) ListListeners(opts listeners.ListOptsBuilder) ([]listeners.Listener, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "list") + allPages, err := listeners.List(l.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return listeners.ExtractListeners(allPages) +} + +func (l lbClient) GetListener(id string) (*listeners.Listener, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "get") + listener, err := listeners.Get(context.TODO(), l.serviceClient, id).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return listener, nil +} + +func (l lbClient) DeleteListener(id string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_listener", "delete") + err := listeners.Delete(context.TODO(), l.serviceClient, id).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return fmt.Errorf("error deleting lbaas listener %s: %v", id, err) + } + return nil +} + +func (l lbClient) CreatePool(opts pools.CreateOptsBuilder) (*pools.Pool, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "create") + pool, err := pools.Create(context.TODO(), l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pool, nil +} + +func (l lbClient) ListPools(opts pools.ListOptsBuilder) ([]pools.Pool, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "list") + allPages, err := pools.List(l.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pools.ExtractPools(allPages) +} + +func (l lbClient) GetPool(id string) (*pools.Pool, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "get") + pool, err := pools.Get(context.TODO(), l.serviceClient, id).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pool, nil +} + +func (l lbClient) DeletePool(id string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "delete") + err := pools.Delete(context.TODO(), l.serviceClient, id).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return fmt.Errorf("error deleting lbaas pool %s: %v", id, err) + } + return nil +} + +func (l lbClient) CreatePoolMember(poolID string, lbMemberOpts pools.CreateMemberOptsBuilder) (*pools.Member, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "create") + member, err := pools.CreateMember(context.TODO(), l.serviceClient, poolID, lbMemberOpts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, fmt.Errorf("error create lbmember: %s", err) + } + return member, nil +} + +func (l lbClient) ListPoolMember(poolID string, opts pools.ListMembersOptsBuilder) ([]pools.Member, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_pool", "list") + allPages, err := pools.ListMembers(l.serviceClient, poolID, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return pools.ExtractMembers(allPages) +} + +func (l lbClient) DeletePoolMember(poolID string, lbMemberID string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_member", "delete") + err := pools.DeleteMember(context.TODO(), l.serviceClient, poolID, lbMemberID).ExtractErr() + if mc.ObserveRequest(err) != nil { + return fmt.Errorf("error deleting lbmember: %s", err) + } + return nil +} + +func (l lbClient) CreateMonitor(opts monitors.CreateOptsBuilder) (*monitors.Monitor, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "create") + monitor, err := monitors.Create(context.TODO(), l.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return monitor, nil +} + +func (l lbClient) ListMonitors(opts monitors.ListOptsBuilder) ([]monitors.Monitor, error) { + mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "list") + allPages, err := monitors.List(l.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return monitors.ExtractMonitors(allPages) +} + +func (l lbClient) DeleteMonitor(id string) error { + mc := metrics.NewMetricPrometheusContext("loadbalancer_healthmonitor", "delete") + err := monitors.Delete(context.TODO(), l.serviceClient, id).ExtractErr() + if mc.ObserveRequestIgnoreNotFound(err) != nil && !capoerrors.IsNotFound(err) { + return fmt.Errorf("error deleting lbaas monitor %s: %v", id, err) + } + return nil +} + +func (l lbClient) ListLoadBalancerProviders() ([]providers.Provider, error) { + allPages, err := providers.List(l.serviceClient, providers.ListOpts{}).AllPages(context.TODO()) + if err != nil { + return nil, fmt.Errorf("listing providers: %v", err) + } + providersList, err := providers.ExtractProviders(allPages) + if err != nil { + return nil, fmt.Errorf("extracting loadbalancer providers pages: %v", err) + } + return providersList, nil +} + +func (l lbClient) ListOctaviaVersions() ([]apiversions.APIVersion, error) { + mc := metrics.NewMetricPrometheusContext("version", "list") + allPages, err := apiversions.List(l.serviceClient).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return apiversions.ExtractAPIVersions(allPages) +} + +func (l lbClient) ListLoadBalancerFlavors() ([]flavors.Flavor, error) { + allPages, err := flavors.List(l.serviceClient, flavors.ListOpts{}).AllPages(context.TODO()) + if err != nil { + return nil, fmt.Errorf("listing flavors: %v", err) + } + flavorList, err := flavors.ExtractFlavors(allPages) + if err != nil { + return nil, fmt.Errorf("extracting loadbalancer flavors pages: %v", err) + } + return flavorList, nil +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/compute.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/compute.go new file mode 100644 index 000000000..c83d7a701 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/compute.go @@ -0,0 +1,224 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/cluster-api-provider-openstack/pkg/clients (interfaces: ComputeClient) +// +// Generated by this command: +// +// mockgen -package mock -destination=compute.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients ComputeClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + attachinterfaces "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/attachinterfaces" + availabilityzones "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/availabilityzones" + flavors "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/flavors" + servergroups "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servergroups" + servers "github.com/gophercloud/gophercloud/v2/openstack/compute/v2/servers" + gomock "go.uber.org/mock/gomock" + clients "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients" +) + +// MockComputeClient is a mock of ComputeClient interface. +type MockComputeClient struct { + ctrl *gomock.Controller + recorder *MockComputeClientMockRecorder + isgomock struct{} +} + +// MockComputeClientMockRecorder is the mock recorder for MockComputeClient. +type MockComputeClientMockRecorder struct { + mock *MockComputeClient +} + +// NewMockComputeClient creates a new mock instance. +func NewMockComputeClient(ctrl *gomock.Controller) *MockComputeClient { + mock := &MockComputeClient{ctrl: ctrl} + mock.recorder = &MockComputeClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockComputeClient) EXPECT() *MockComputeClientMockRecorder { + return m.recorder +} + +// CreateServer mocks base method. +func (m *MockComputeClient) CreateServer(createOpts servers.CreateOptsBuilder, schedulerHints servers.SchedulerHintOptsBuilder) (*servers.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateServer", createOpts, schedulerHints) + ret0, _ := ret[0].(*servers.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateServer indicates an expected call of CreateServer. +func (mr *MockComputeClientMockRecorder) CreateServer(createOpts, schedulerHints any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServer", reflect.TypeOf((*MockComputeClient)(nil).CreateServer), createOpts, schedulerHints) +} + +// DeleteAttachedInterface mocks base method. +func (m *MockComputeClient) DeleteAttachedInterface(serverID, portID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAttachedInterface", serverID, portID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteAttachedInterface indicates an expected call of DeleteAttachedInterface. +func (mr *MockComputeClientMockRecorder) DeleteAttachedInterface(serverID, portID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAttachedInterface", reflect.TypeOf((*MockComputeClient)(nil).DeleteAttachedInterface), serverID, portID) +} + +// DeleteServer mocks base method. +func (m *MockComputeClient) DeleteServer(serverID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteServer", serverID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteServer indicates an expected call of DeleteServer. +func (mr *MockComputeClientMockRecorder) DeleteServer(serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteServer", reflect.TypeOf((*MockComputeClient)(nil).DeleteServer), serverID) +} + +// GetConsoleOutput mocks base method. +func (m *MockComputeClient) GetConsoleOutput(serverID string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConsoleOutput", serverID) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetConsoleOutput indicates an expected call of GetConsoleOutput. +func (mr *MockComputeClientMockRecorder) GetConsoleOutput(serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConsoleOutput", reflect.TypeOf((*MockComputeClient)(nil).GetConsoleOutput), serverID) +} + +// GetServer mocks base method. +func (m *MockComputeClient) GetServer(serverID string) (*servers.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetServer", serverID) + ret0, _ := ret[0].(*servers.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetServer indicates an expected call of GetServer. +func (mr *MockComputeClientMockRecorder) GetServer(serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetServer", reflect.TypeOf((*MockComputeClient)(nil).GetServer), serverID) +} + +// ListAttachedInterfaces mocks base method. +func (m *MockComputeClient) ListAttachedInterfaces(serverID string) ([]attachinterfaces.Interface, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAttachedInterfaces", serverID) + ret0, _ := ret[0].([]attachinterfaces.Interface) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAttachedInterfaces indicates an expected call of ListAttachedInterfaces. +func (mr *MockComputeClientMockRecorder) ListAttachedInterfaces(serverID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAttachedInterfaces", reflect.TypeOf((*MockComputeClient)(nil).ListAttachedInterfaces), serverID) +} + +// ListAvailabilityZones mocks base method. +func (m *MockComputeClient) ListAvailabilityZones() ([]availabilityzones.AvailabilityZone, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAvailabilityZones") + ret0, _ := ret[0].([]availabilityzones.AvailabilityZone) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAvailabilityZones indicates an expected call of ListAvailabilityZones. +func (mr *MockComputeClientMockRecorder) ListAvailabilityZones() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAvailabilityZones", reflect.TypeOf((*MockComputeClient)(nil).ListAvailabilityZones)) +} + +// ListFlavors mocks base method. +func (m *MockComputeClient) ListFlavors() ([]flavors.Flavor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListFlavors") + ret0, _ := ret[0].([]flavors.Flavor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListFlavors indicates an expected call of ListFlavors. +func (mr *MockComputeClientMockRecorder) ListFlavors() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFlavors", reflect.TypeOf((*MockComputeClient)(nil).ListFlavors)) +} + +// ListServerGroups mocks base method. +func (m *MockComputeClient) ListServerGroups() ([]servergroups.ServerGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListServerGroups") + ret0, _ := ret[0].([]servergroups.ServerGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListServerGroups indicates an expected call of ListServerGroups. +func (mr *MockComputeClientMockRecorder) ListServerGroups() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServerGroups", reflect.TypeOf((*MockComputeClient)(nil).ListServerGroups)) +} + +// ListServers mocks base method. +func (m *MockComputeClient) ListServers(listOpts servers.ListOptsBuilder) ([]servers.Server, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListServers", listOpts) + ret0, _ := ret[0].([]servers.Server) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListServers indicates an expected call of ListServers. +func (mr *MockComputeClientMockRecorder) ListServers(listOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListServers", reflect.TypeOf((*MockComputeClient)(nil).ListServers), listOpts) +} + +// WithMicroversion mocks base method. +func (m *MockComputeClient) WithMicroversion(required string) (clients.ComputeClient, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WithMicroversion", required) + ret0, _ := ret[0].(clients.ComputeClient) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WithMicroversion indicates an expected call of WithMicroversion. +func (mr *MockComputeClientMockRecorder) WithMicroversion(required any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithMicroversion", reflect.TypeOf((*MockComputeClient)(nil).WithMicroversion), required) +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/doc.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/doc.go new file mode 100644 index 000000000..616fcbdcd --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/doc.go @@ -0,0 +1,38 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mock + +import ( + // Runtime dependency of mockgen, required when using vendoring so go mod knows + // to pull it in. + _ "go.uber.org/mock/mockgen/model" +) + +//go:generate mockgen -package mock -destination=compute.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients ComputeClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate/boilerplate.generatego.txt compute.go > _compute.go && mv _compute.go compute.go" + +//go:generate mockgen -package mock -destination=image.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients ImageClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate/boilerplate.generatego.txt image.go > _image.go && mv _image.go image.go" + +//go:generate mockgen -package mock -destination=loadbalancer.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients LbClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate/boilerplate.generatego.txt loadbalancer.go > _loadbalancer.go && mv _loadbalancer.go loadbalancer.go" + +//go:generate mockgen -package mock -destination=network.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients NetworkClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate/boilerplate.generatego.txt network.go > _network.go && mv _network.go network.go" + +//go:generate mockgen -package mock -destination=volume.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients VolumeClient +//go:generate /usr/bin/env bash -c "cat ../../../hack/boilerplate/boilerplate.generatego.txt volume.go > _volume.go && mv _volume.go volume.go" diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/image.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/image.go new file mode 100644 index 000000000..944268573 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/image.go @@ -0,0 +1,161 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/cluster-api-provider-openstack/pkg/clients (interfaces: ImageClient) +// +// Generated by this command: +// +// mockgen -package mock -destination=image.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients ImageClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + context "context" + io "io" + reflect "reflect" + + imageimport "github.com/gophercloud/gophercloud/v2/openstack/image/v2/imageimport" + images "github.com/gophercloud/gophercloud/v2/openstack/image/v2/images" + gomock "go.uber.org/mock/gomock" +) + +// MockImageClient is a mock of ImageClient interface. +type MockImageClient struct { + ctrl *gomock.Controller + recorder *MockImageClientMockRecorder + isgomock struct{} +} + +// MockImageClientMockRecorder is the mock recorder for MockImageClient. +type MockImageClientMockRecorder struct { + mock *MockImageClient +} + +// NewMockImageClient creates a new mock instance. +func NewMockImageClient(ctrl *gomock.Controller) *MockImageClient { + mock := &MockImageClient{ctrl: ctrl} + mock.recorder = &MockImageClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockImageClient) EXPECT() *MockImageClientMockRecorder { + return m.recorder +} + +// CreateImage mocks base method. +func (m *MockImageClient) CreateImage(ctx context.Context, createOpts images.CreateOptsBuilder) (*images.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateImage", ctx, createOpts) + ret0, _ := ret[0].(*images.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateImage indicates an expected call of CreateImage. +func (mr *MockImageClientMockRecorder) CreateImage(ctx, createOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateImage", reflect.TypeOf((*MockImageClient)(nil).CreateImage), ctx, createOpts) +} + +// CreateImport mocks base method. +func (m *MockImageClient) CreateImport(ctx context.Context, id string, createOpts imageimport.CreateOptsBuilder) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateImport", ctx, id, createOpts) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateImport indicates an expected call of CreateImport. +func (mr *MockImageClientMockRecorder) CreateImport(ctx, id, createOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateImport", reflect.TypeOf((*MockImageClient)(nil).CreateImport), ctx, id, createOpts) +} + +// DeleteImage mocks base method. +func (m *MockImageClient) DeleteImage(ctx context.Context, id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteImage", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteImage indicates an expected call of DeleteImage. +func (mr *MockImageClientMockRecorder) DeleteImage(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteImage", reflect.TypeOf((*MockImageClient)(nil).DeleteImage), ctx, id) +} + +// GetImage mocks base method. +func (m *MockImageClient) GetImage(id string) (*images.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImage", id) + ret0, _ := ret[0].(*images.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetImage indicates an expected call of GetImage. +func (mr *MockImageClientMockRecorder) GetImage(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImage", reflect.TypeOf((*MockImageClient)(nil).GetImage), id) +} + +// GetImportInfo mocks base method. +func (m *MockImageClient) GetImportInfo(ctx context.Context) (*imageimport.ImportInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetImportInfo", ctx) + ret0, _ := ret[0].(*imageimport.ImportInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetImportInfo indicates an expected call of GetImportInfo. +func (mr *MockImageClientMockRecorder) GetImportInfo(ctx any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetImportInfo", reflect.TypeOf((*MockImageClient)(nil).GetImportInfo), ctx) +} + +// ListImages mocks base method. +func (m *MockImageClient) ListImages(listOpts images.ListOptsBuilder) ([]images.Image, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListImages", listOpts) + ret0, _ := ret[0].([]images.Image) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListImages indicates an expected call of ListImages. +func (mr *MockImageClientMockRecorder) ListImages(listOpts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImages", reflect.TypeOf((*MockImageClient)(nil).ListImages), listOpts) +} + +// UploadData mocks base method. +func (m *MockImageClient) UploadData(ctx context.Context, id string, data io.Reader) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UploadData", ctx, id, data) + ret0, _ := ret[0].(error) + return ret0 +} + +// UploadData indicates an expected call of UploadData. +func (mr *MockImageClientMockRecorder) UploadData(ctx, id, data any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadData", reflect.TypeOf((*MockImageClient)(nil).UploadData), ctx, id, data) +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/loadbalancer.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/loadbalancer.go new file mode 100644 index 000000000..92b15886c --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/loadbalancer.go @@ -0,0 +1,387 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/cluster-api-provider-openstack/pkg/clients (interfaces: LbClient) +// +// Generated by this command: +// +// mockgen -package mock -destination=loadbalancer.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients LbClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + apiversions "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/apiversions" + flavors "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/flavors" + listeners "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/listeners" + loadbalancers "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/loadbalancers" + monitors "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/monitors" + pools "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/pools" + providers "github.com/gophercloud/gophercloud/v2/openstack/loadbalancer/v2/providers" + gomock "go.uber.org/mock/gomock" +) + +// MockLbClient is a mock of LbClient interface. +type MockLbClient struct { + ctrl *gomock.Controller + recorder *MockLbClientMockRecorder + isgomock struct{} +} + +// MockLbClientMockRecorder is the mock recorder for MockLbClient. +type MockLbClientMockRecorder struct { + mock *MockLbClient +} + +// NewMockLbClient creates a new mock instance. +func NewMockLbClient(ctrl *gomock.Controller) *MockLbClient { + mock := &MockLbClient{ctrl: ctrl} + mock.recorder = &MockLbClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLbClient) EXPECT() *MockLbClientMockRecorder { + return m.recorder +} + +// CreateListener mocks base method. +func (m *MockLbClient) CreateListener(opts listeners.CreateOptsBuilder) (*listeners.Listener, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateListener", opts) + ret0, _ := ret[0].(*listeners.Listener) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateListener indicates an expected call of CreateListener. +func (mr *MockLbClientMockRecorder) CreateListener(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateListener", reflect.TypeOf((*MockLbClient)(nil).CreateListener), opts) +} + +// CreateLoadBalancer mocks base method. +func (m *MockLbClient) CreateLoadBalancer(opts loadbalancers.CreateOptsBuilder) (*loadbalancers.LoadBalancer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateLoadBalancer", opts) + ret0, _ := ret[0].(*loadbalancers.LoadBalancer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateLoadBalancer indicates an expected call of CreateLoadBalancer. +func (mr *MockLbClientMockRecorder) CreateLoadBalancer(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateLoadBalancer", reflect.TypeOf((*MockLbClient)(nil).CreateLoadBalancer), opts) +} + +// CreateMonitor mocks base method. +func (m *MockLbClient) CreateMonitor(opts monitors.CreateOptsBuilder) (*monitors.Monitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateMonitor", opts) + ret0, _ := ret[0].(*monitors.Monitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateMonitor indicates an expected call of CreateMonitor. +func (mr *MockLbClientMockRecorder) CreateMonitor(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMonitor", reflect.TypeOf((*MockLbClient)(nil).CreateMonitor), opts) +} + +// CreatePool mocks base method. +func (m *MockLbClient) CreatePool(opts pools.CreateOptsBuilder) (*pools.Pool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePool", opts) + ret0, _ := ret[0].(*pools.Pool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePool indicates an expected call of CreatePool. +func (mr *MockLbClientMockRecorder) CreatePool(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePool", reflect.TypeOf((*MockLbClient)(nil).CreatePool), opts) +} + +// CreatePoolMember mocks base method. +func (m *MockLbClient) CreatePoolMember(poolID string, opts pools.CreateMemberOptsBuilder) (*pools.Member, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePoolMember", poolID, opts) + ret0, _ := ret[0].(*pools.Member) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePoolMember indicates an expected call of CreatePoolMember. +func (mr *MockLbClientMockRecorder) CreatePoolMember(poolID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePoolMember", reflect.TypeOf((*MockLbClient)(nil).CreatePoolMember), poolID, opts) +} + +// DeleteListener mocks base method. +func (m *MockLbClient) DeleteListener(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteListener", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteListener indicates an expected call of DeleteListener. +func (mr *MockLbClientMockRecorder) DeleteListener(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteListener", reflect.TypeOf((*MockLbClient)(nil).DeleteListener), id) +} + +// DeleteLoadBalancer mocks base method. +func (m *MockLbClient) DeleteLoadBalancer(id string, opts loadbalancers.DeleteOptsBuilder) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteLoadBalancer", id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteLoadBalancer indicates an expected call of DeleteLoadBalancer. +func (mr *MockLbClientMockRecorder) DeleteLoadBalancer(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteLoadBalancer", reflect.TypeOf((*MockLbClient)(nil).DeleteLoadBalancer), id, opts) +} + +// DeleteMonitor mocks base method. +func (m *MockLbClient) DeleteMonitor(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteMonitor", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteMonitor indicates an expected call of DeleteMonitor. +func (mr *MockLbClientMockRecorder) DeleteMonitor(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMonitor", reflect.TypeOf((*MockLbClient)(nil).DeleteMonitor), id) +} + +// DeletePool mocks base method. +func (m *MockLbClient) DeletePool(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePool", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePool indicates an expected call of DeletePool. +func (mr *MockLbClientMockRecorder) DeletePool(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePool", reflect.TypeOf((*MockLbClient)(nil).DeletePool), id) +} + +// DeletePoolMember mocks base method. +func (m *MockLbClient) DeletePoolMember(poolID, lbMemberID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePoolMember", poolID, lbMemberID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePoolMember indicates an expected call of DeletePoolMember. +func (mr *MockLbClientMockRecorder) DeletePoolMember(poolID, lbMemberID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePoolMember", reflect.TypeOf((*MockLbClient)(nil).DeletePoolMember), poolID, lbMemberID) +} + +// GetListener mocks base method. +func (m *MockLbClient) GetListener(id string) (*listeners.Listener, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetListener", id) + ret0, _ := ret[0].(*listeners.Listener) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetListener indicates an expected call of GetListener. +func (mr *MockLbClientMockRecorder) GetListener(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetListener", reflect.TypeOf((*MockLbClient)(nil).GetListener), id) +} + +// GetLoadBalancer mocks base method. +func (m *MockLbClient) GetLoadBalancer(id string) (*loadbalancers.LoadBalancer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLoadBalancer", id) + ret0, _ := ret[0].(*loadbalancers.LoadBalancer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLoadBalancer indicates an expected call of GetLoadBalancer. +func (mr *MockLbClientMockRecorder) GetLoadBalancer(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLoadBalancer", reflect.TypeOf((*MockLbClient)(nil).GetLoadBalancer), id) +} + +// GetPool mocks base method. +func (m *MockLbClient) GetPool(id string) (*pools.Pool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPool", id) + ret0, _ := ret[0].(*pools.Pool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPool indicates an expected call of GetPool. +func (mr *MockLbClientMockRecorder) GetPool(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPool", reflect.TypeOf((*MockLbClient)(nil).GetPool), id) +} + +// ListListeners mocks base method. +func (m *MockLbClient) ListListeners(opts listeners.ListOptsBuilder) ([]listeners.Listener, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListListeners", opts) + ret0, _ := ret[0].([]listeners.Listener) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListListeners indicates an expected call of ListListeners. +func (mr *MockLbClientMockRecorder) ListListeners(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListListeners", reflect.TypeOf((*MockLbClient)(nil).ListListeners), opts) +} + +// ListLoadBalancerFlavors mocks base method. +func (m *MockLbClient) ListLoadBalancerFlavors() ([]flavors.Flavor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLoadBalancerFlavors") + ret0, _ := ret[0].([]flavors.Flavor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLoadBalancerFlavors indicates an expected call of ListLoadBalancerFlavors. +func (mr *MockLbClientMockRecorder) ListLoadBalancerFlavors() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLoadBalancerFlavors", reflect.TypeOf((*MockLbClient)(nil).ListLoadBalancerFlavors)) +} + +// ListLoadBalancerProviders mocks base method. +func (m *MockLbClient) ListLoadBalancerProviders() ([]providers.Provider, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLoadBalancerProviders") + ret0, _ := ret[0].([]providers.Provider) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLoadBalancerProviders indicates an expected call of ListLoadBalancerProviders. +func (mr *MockLbClientMockRecorder) ListLoadBalancerProviders() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLoadBalancerProviders", reflect.TypeOf((*MockLbClient)(nil).ListLoadBalancerProviders)) +} + +// ListLoadBalancers mocks base method. +func (m *MockLbClient) ListLoadBalancers(opts loadbalancers.ListOptsBuilder) ([]loadbalancers.LoadBalancer, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListLoadBalancers", opts) + ret0, _ := ret[0].([]loadbalancers.LoadBalancer) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListLoadBalancers indicates an expected call of ListLoadBalancers. +func (mr *MockLbClientMockRecorder) ListLoadBalancers(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListLoadBalancers", reflect.TypeOf((*MockLbClient)(nil).ListLoadBalancers), opts) +} + +// ListMonitors mocks base method. +func (m *MockLbClient) ListMonitors(opts monitors.ListOptsBuilder) ([]monitors.Monitor, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListMonitors", opts) + ret0, _ := ret[0].([]monitors.Monitor) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListMonitors indicates an expected call of ListMonitors. +func (mr *MockLbClientMockRecorder) ListMonitors(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMonitors", reflect.TypeOf((*MockLbClient)(nil).ListMonitors), opts) +} + +// ListOctaviaVersions mocks base method. +func (m *MockLbClient) ListOctaviaVersions() ([]apiversions.APIVersion, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListOctaviaVersions") + ret0, _ := ret[0].([]apiversions.APIVersion) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListOctaviaVersions indicates an expected call of ListOctaviaVersions. +func (mr *MockLbClientMockRecorder) ListOctaviaVersions() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListOctaviaVersions", reflect.TypeOf((*MockLbClient)(nil).ListOctaviaVersions)) +} + +// ListPoolMember mocks base method. +func (m *MockLbClient) ListPoolMember(poolID string, opts pools.ListMembersOptsBuilder) ([]pools.Member, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPoolMember", poolID, opts) + ret0, _ := ret[0].([]pools.Member) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPoolMember indicates an expected call of ListPoolMember. +func (mr *MockLbClientMockRecorder) ListPoolMember(poolID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPoolMember", reflect.TypeOf((*MockLbClient)(nil).ListPoolMember), poolID, opts) +} + +// ListPools mocks base method. +func (m *MockLbClient) ListPools(opts pools.ListOptsBuilder) ([]pools.Pool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPools", opts) + ret0, _ := ret[0].([]pools.Pool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPools indicates an expected call of ListPools. +func (mr *MockLbClientMockRecorder) ListPools(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPools", reflect.TypeOf((*MockLbClient)(nil).ListPools), opts) +} + +// UpdateListener mocks base method. +func (m *MockLbClient) UpdateListener(id string, opts listeners.UpdateOpts) (*listeners.Listener, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateListener", id, opts) + ret0, _ := ret[0].(*listeners.Listener) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateListener indicates an expected call of UpdateListener. +func (mr *MockLbClientMockRecorder) UpdateListener(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateListener", reflect.TypeOf((*MockLbClient)(nil).UpdateListener), id, opts) +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/network.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/network.go new file mode 100644 index 000000000..1dfafc4d0 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/network.go @@ -0,0 +1,701 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/cluster-api-provider-openstack/pkg/clients (interfaces: NetworkClient) +// +// Generated by this command: +// +// mockgen -package mock -destination=network.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients NetworkClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + extensions "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions" + attributestags "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags" + floatingips "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips" + routers "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers" + groups "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups" + rules "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules" + trunks "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks" + networks "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" + ports "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports" + subnets "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" + gomock "go.uber.org/mock/gomock" +) + +// MockNetworkClient is a mock of NetworkClient interface. +type MockNetworkClient struct { + ctrl *gomock.Controller + recorder *MockNetworkClientMockRecorder + isgomock struct{} +} + +// MockNetworkClientMockRecorder is the mock recorder for MockNetworkClient. +type MockNetworkClientMockRecorder struct { + mock *MockNetworkClient +} + +// NewMockNetworkClient creates a new mock instance. +func NewMockNetworkClient(ctrl *gomock.Controller) *MockNetworkClient { + mock := &MockNetworkClient{ctrl: ctrl} + mock.recorder = &MockNetworkClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockNetworkClient) EXPECT() *MockNetworkClientMockRecorder { + return m.recorder +} + +// AddRouterInterface mocks base method. +func (m *MockNetworkClient) AddRouterInterface(id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddRouterInterface", id, opts) + ret0, _ := ret[0].(*routers.InterfaceInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddRouterInterface indicates an expected call of AddRouterInterface. +func (mr *MockNetworkClientMockRecorder) AddRouterInterface(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddRouterInterface", reflect.TypeOf((*MockNetworkClient)(nil).AddRouterInterface), id, opts) +} + +// CreateFloatingIP mocks base method. +func (m *MockNetworkClient) CreateFloatingIP(opts floatingips.CreateOptsBuilder) (*floatingips.FloatingIP, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateFloatingIP", opts) + ret0, _ := ret[0].(*floatingips.FloatingIP) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateFloatingIP indicates an expected call of CreateFloatingIP. +func (mr *MockNetworkClientMockRecorder) CreateFloatingIP(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateFloatingIP", reflect.TypeOf((*MockNetworkClient)(nil).CreateFloatingIP), opts) +} + +// CreateNetwork mocks base method. +func (m *MockNetworkClient) CreateNetwork(opts networks.CreateOptsBuilder) (*networks.Network, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateNetwork", opts) + ret0, _ := ret[0].(*networks.Network) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateNetwork indicates an expected call of CreateNetwork. +func (mr *MockNetworkClientMockRecorder) CreateNetwork(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateNetwork", reflect.TypeOf((*MockNetworkClient)(nil).CreateNetwork), opts) +} + +// CreatePort mocks base method. +func (m *MockNetworkClient) CreatePort(opts ports.CreateOptsBuilder) (*ports.Port, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePort", opts) + ret0, _ := ret[0].(*ports.Port) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePort indicates an expected call of CreatePort. +func (mr *MockNetworkClientMockRecorder) CreatePort(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePort", reflect.TypeOf((*MockNetworkClient)(nil).CreatePort), opts) +} + +// CreateRouter mocks base method. +func (m *MockNetworkClient) CreateRouter(opts routers.CreateOptsBuilder) (*routers.Router, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateRouter", opts) + ret0, _ := ret[0].(*routers.Router) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateRouter indicates an expected call of CreateRouter. +func (mr *MockNetworkClientMockRecorder) CreateRouter(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRouter", reflect.TypeOf((*MockNetworkClient)(nil).CreateRouter), opts) +} + +// CreateSecGroup mocks base method. +func (m *MockNetworkClient) CreateSecGroup(opts groups.CreateOptsBuilder) (*groups.SecGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSecGroup", opts) + ret0, _ := ret[0].(*groups.SecGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSecGroup indicates an expected call of CreateSecGroup. +func (mr *MockNetworkClientMockRecorder) CreateSecGroup(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).CreateSecGroup), opts) +} + +// CreateSecGroupRule mocks base method. +func (m *MockNetworkClient) CreateSecGroupRule(opts rules.CreateOptsBuilder) (*rules.SecGroupRule, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSecGroupRule", opts) + ret0, _ := ret[0].(*rules.SecGroupRule) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSecGroupRule indicates an expected call of CreateSecGroupRule. +func (mr *MockNetworkClientMockRecorder) CreateSecGroupRule(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).CreateSecGroupRule), opts) +} + +// CreateSubnet mocks base method. +func (m *MockNetworkClient) CreateSubnet(opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSubnet", opts) + ret0, _ := ret[0].(*subnets.Subnet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSubnet indicates an expected call of CreateSubnet. +func (mr *MockNetworkClientMockRecorder) CreateSubnet(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubnet", reflect.TypeOf((*MockNetworkClient)(nil).CreateSubnet), opts) +} + +// CreateTrunk mocks base method. +func (m *MockNetworkClient) CreateTrunk(opts trunks.CreateOptsBuilder) (*trunks.Trunk, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTrunk", opts) + ret0, _ := ret[0].(*trunks.Trunk) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateTrunk indicates an expected call of CreateTrunk. +func (mr *MockNetworkClientMockRecorder) CreateTrunk(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTrunk", reflect.TypeOf((*MockNetworkClient)(nil).CreateTrunk), opts) +} + +// DeleteFloatingIP mocks base method. +func (m *MockNetworkClient) DeleteFloatingIP(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteFloatingIP", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteFloatingIP indicates an expected call of DeleteFloatingIP. +func (mr *MockNetworkClientMockRecorder) DeleteFloatingIP(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteFloatingIP", reflect.TypeOf((*MockNetworkClient)(nil).DeleteFloatingIP), id) +} + +// DeleteNetwork mocks base method. +func (m *MockNetworkClient) DeleteNetwork(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteNetwork", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteNetwork indicates an expected call of DeleteNetwork. +func (mr *MockNetworkClientMockRecorder) DeleteNetwork(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteNetwork", reflect.TypeOf((*MockNetworkClient)(nil).DeleteNetwork), id) +} + +// DeletePort mocks base method. +func (m *MockNetworkClient) DeletePort(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeletePort", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeletePort indicates an expected call of DeletePort. +func (mr *MockNetworkClientMockRecorder) DeletePort(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePort", reflect.TypeOf((*MockNetworkClient)(nil).DeletePort), id) +} + +// DeleteRouter mocks base method. +func (m *MockNetworkClient) DeleteRouter(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteRouter", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteRouter indicates an expected call of DeleteRouter. +func (mr *MockNetworkClientMockRecorder) DeleteRouter(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteRouter", reflect.TypeOf((*MockNetworkClient)(nil).DeleteRouter), id) +} + +// DeleteSecGroup mocks base method. +func (m *MockNetworkClient) DeleteSecGroup(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSecGroup", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSecGroup indicates an expected call of DeleteSecGroup. +func (mr *MockNetworkClientMockRecorder) DeleteSecGroup(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSecGroup), id) +} + +// DeleteSecGroupRule mocks base method. +func (m *MockNetworkClient) DeleteSecGroupRule(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSecGroupRule", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSecGroupRule indicates an expected call of DeleteSecGroupRule. +func (mr *MockNetworkClientMockRecorder) DeleteSecGroupRule(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSecGroupRule), id) +} + +// DeleteSubnet mocks base method. +func (m *MockNetworkClient) DeleteSubnet(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSubnet", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSubnet indicates an expected call of DeleteSubnet. +func (mr *MockNetworkClientMockRecorder) DeleteSubnet(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubnet", reflect.TypeOf((*MockNetworkClient)(nil).DeleteSubnet), id) +} + +// DeleteTrunk mocks base method. +func (m *MockNetworkClient) DeleteTrunk(id string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteTrunk", id) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteTrunk indicates an expected call of DeleteTrunk. +func (mr *MockNetworkClientMockRecorder) DeleteTrunk(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTrunk", reflect.TypeOf((*MockNetworkClient)(nil).DeleteTrunk), id) +} + +// GetFloatingIP mocks base method. +func (m *MockNetworkClient) GetFloatingIP(id string) (*floatingips.FloatingIP, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetFloatingIP", id) + ret0, _ := ret[0].(*floatingips.FloatingIP) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetFloatingIP indicates an expected call of GetFloatingIP. +func (mr *MockNetworkClientMockRecorder) GetFloatingIP(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFloatingIP", reflect.TypeOf((*MockNetworkClient)(nil).GetFloatingIP), id) +} + +// GetNetwork mocks base method. +func (m *MockNetworkClient) GetNetwork(id string) (*networks.Network, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetNetwork", id) + ret0, _ := ret[0].(*networks.Network) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNetwork indicates an expected call of GetNetwork. +func (mr *MockNetworkClientMockRecorder) GetNetwork(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNetwork", reflect.TypeOf((*MockNetworkClient)(nil).GetNetwork), id) +} + +// GetPort mocks base method. +func (m *MockNetworkClient) GetPort(id string) (*ports.Port, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetPort", id) + ret0, _ := ret[0].(*ports.Port) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetPort indicates an expected call of GetPort. +func (mr *MockNetworkClientMockRecorder) GetPort(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPort", reflect.TypeOf((*MockNetworkClient)(nil).GetPort), id) +} + +// GetRouter mocks base method. +func (m *MockNetworkClient) GetRouter(id string) (*routers.Router, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetRouter", id) + ret0, _ := ret[0].(*routers.Router) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetRouter indicates an expected call of GetRouter. +func (mr *MockNetworkClientMockRecorder) GetRouter(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRouter", reflect.TypeOf((*MockNetworkClient)(nil).GetRouter), id) +} + +// GetSecGroup mocks base method. +func (m *MockNetworkClient) GetSecGroup(id string) (*groups.SecGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSecGroup", id) + ret0, _ := ret[0].(*groups.SecGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSecGroup indicates an expected call of GetSecGroup. +func (mr *MockNetworkClientMockRecorder) GetSecGroup(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).GetSecGroup), id) +} + +// GetSecGroupRule mocks base method. +func (m *MockNetworkClient) GetSecGroupRule(id string) (*rules.SecGroupRule, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSecGroupRule", id) + ret0, _ := ret[0].(*rules.SecGroupRule) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSecGroupRule indicates an expected call of GetSecGroupRule. +func (mr *MockNetworkClientMockRecorder) GetSecGroupRule(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).GetSecGroupRule), id) +} + +// GetSubnet mocks base method. +func (m *MockNetworkClient) GetSubnet(id string) (*subnets.Subnet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSubnet", id) + ret0, _ := ret[0].(*subnets.Subnet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubnet indicates an expected call of GetSubnet. +func (mr *MockNetworkClientMockRecorder) GetSubnet(id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubnet", reflect.TypeOf((*MockNetworkClient)(nil).GetSubnet), id) +} + +// ListExtensions mocks base method. +func (m *MockNetworkClient) ListExtensions() ([]extensions.Extension, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListExtensions") + ret0, _ := ret[0].([]extensions.Extension) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListExtensions indicates an expected call of ListExtensions. +func (mr *MockNetworkClientMockRecorder) ListExtensions() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExtensions", reflect.TypeOf((*MockNetworkClient)(nil).ListExtensions)) +} + +// ListFloatingIP mocks base method. +func (m *MockNetworkClient) ListFloatingIP(opts floatingips.ListOptsBuilder) ([]floatingips.FloatingIP, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListFloatingIP", opts) + ret0, _ := ret[0].([]floatingips.FloatingIP) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListFloatingIP indicates an expected call of ListFloatingIP. +func (mr *MockNetworkClientMockRecorder) ListFloatingIP(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListFloatingIP", reflect.TypeOf((*MockNetworkClient)(nil).ListFloatingIP), opts) +} + +// ListNetwork mocks base method. +func (m *MockNetworkClient) ListNetwork(opts networks.ListOptsBuilder) ([]networks.Network, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListNetwork", opts) + ret0, _ := ret[0].([]networks.Network) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListNetwork indicates an expected call of ListNetwork. +func (mr *MockNetworkClientMockRecorder) ListNetwork(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListNetwork", reflect.TypeOf((*MockNetworkClient)(nil).ListNetwork), opts) +} + +// ListPort mocks base method. +func (m *MockNetworkClient) ListPort(opts ports.ListOptsBuilder) ([]ports.Port, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListPort", opts) + ret0, _ := ret[0].([]ports.Port) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListPort indicates an expected call of ListPort. +func (mr *MockNetworkClientMockRecorder) ListPort(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPort", reflect.TypeOf((*MockNetworkClient)(nil).ListPort), opts) +} + +// ListRouter mocks base method. +func (m *MockNetworkClient) ListRouter(opts routers.ListOpts) ([]routers.Router, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListRouter", opts) + ret0, _ := ret[0].([]routers.Router) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListRouter indicates an expected call of ListRouter. +func (mr *MockNetworkClientMockRecorder) ListRouter(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListRouter", reflect.TypeOf((*MockNetworkClient)(nil).ListRouter), opts) +} + +// ListSecGroup mocks base method. +func (m *MockNetworkClient) ListSecGroup(opts groups.ListOpts) ([]groups.SecGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSecGroup", opts) + ret0, _ := ret[0].([]groups.SecGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSecGroup indicates an expected call of ListSecGroup. +func (mr *MockNetworkClientMockRecorder) ListSecGroup(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).ListSecGroup), opts) +} + +// ListSecGroupRule mocks base method. +func (m *MockNetworkClient) ListSecGroupRule(opts rules.ListOpts) ([]rules.SecGroupRule, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSecGroupRule", opts) + ret0, _ := ret[0].([]rules.SecGroupRule) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSecGroupRule indicates an expected call of ListSecGroupRule. +func (mr *MockNetworkClientMockRecorder) ListSecGroupRule(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSecGroupRule", reflect.TypeOf((*MockNetworkClient)(nil).ListSecGroupRule), opts) +} + +// ListSubnet mocks base method. +func (m *MockNetworkClient) ListSubnet(opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListSubnet", opts) + ret0, _ := ret[0].([]subnets.Subnet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListSubnet indicates an expected call of ListSubnet. +func (mr *MockNetworkClientMockRecorder) ListSubnet(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListSubnet", reflect.TypeOf((*MockNetworkClient)(nil).ListSubnet), opts) +} + +// ListTrunk mocks base method. +func (m *MockNetworkClient) ListTrunk(opts trunks.ListOptsBuilder) ([]trunks.Trunk, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListTrunk", opts) + ret0, _ := ret[0].([]trunks.Trunk) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListTrunk indicates an expected call of ListTrunk. +func (mr *MockNetworkClientMockRecorder) ListTrunk(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTrunk", reflect.TypeOf((*MockNetworkClient)(nil).ListTrunk), opts) +} + +// ListTrunkSubports mocks base method. +func (m *MockNetworkClient) ListTrunkSubports(trunkID string) ([]trunks.Subport, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListTrunkSubports", trunkID) + ret0, _ := ret[0].([]trunks.Subport) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListTrunkSubports indicates an expected call of ListTrunkSubports. +func (mr *MockNetworkClientMockRecorder) ListTrunkSubports(trunkID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTrunkSubports", reflect.TypeOf((*MockNetworkClient)(nil).ListTrunkSubports), trunkID) +} + +// RemoveRouterInterface mocks base method. +func (m *MockNetworkClient) RemoveRouterInterface(id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveRouterInterface", id, opts) + ret0, _ := ret[0].(*routers.InterfaceInfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RemoveRouterInterface indicates an expected call of RemoveRouterInterface. +func (mr *MockNetworkClientMockRecorder) RemoveRouterInterface(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveRouterInterface", reflect.TypeOf((*MockNetworkClient)(nil).RemoveRouterInterface), id, opts) +} + +// RemoveSubports mocks base method. +func (m *MockNetworkClient) RemoveSubports(id string, opts trunks.RemoveSubportsOpts) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveSubports", id, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveSubports indicates an expected call of RemoveSubports. +func (mr *MockNetworkClientMockRecorder) RemoveSubports(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveSubports", reflect.TypeOf((*MockNetworkClient)(nil).RemoveSubports), id, opts) +} + +// ReplaceAllAttributesTags mocks base method. +func (m *MockNetworkClient) ReplaceAllAttributesTags(resourceType, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReplaceAllAttributesTags", resourceType, resourceID, opts) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReplaceAllAttributesTags indicates an expected call of ReplaceAllAttributesTags. +func (mr *MockNetworkClientMockRecorder) ReplaceAllAttributesTags(resourceType, resourceID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReplaceAllAttributesTags", reflect.TypeOf((*MockNetworkClient)(nil).ReplaceAllAttributesTags), resourceType, resourceID, opts) +} + +// UpdateFloatingIP mocks base method. +func (m *MockNetworkClient) UpdateFloatingIP(id string, opts floatingips.UpdateOptsBuilder) (*floatingips.FloatingIP, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateFloatingIP", id, opts) + ret0, _ := ret[0].(*floatingips.FloatingIP) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateFloatingIP indicates an expected call of UpdateFloatingIP. +func (mr *MockNetworkClientMockRecorder) UpdateFloatingIP(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateFloatingIP", reflect.TypeOf((*MockNetworkClient)(nil).UpdateFloatingIP), id, opts) +} + +// UpdateNetwork mocks base method. +func (m *MockNetworkClient) UpdateNetwork(id string, opts networks.UpdateOptsBuilder) (*networks.Network, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateNetwork", id, opts) + ret0, _ := ret[0].(*networks.Network) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateNetwork indicates an expected call of UpdateNetwork. +func (mr *MockNetworkClientMockRecorder) UpdateNetwork(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateNetwork", reflect.TypeOf((*MockNetworkClient)(nil).UpdateNetwork), id, opts) +} + +// UpdatePort mocks base method. +func (m *MockNetworkClient) UpdatePort(id string, opts ports.UpdateOptsBuilder) (*ports.Port, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePort", id, opts) + ret0, _ := ret[0].(*ports.Port) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdatePort indicates an expected call of UpdatePort. +func (mr *MockNetworkClientMockRecorder) UpdatePort(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePort", reflect.TypeOf((*MockNetworkClient)(nil).UpdatePort), id, opts) +} + +// UpdateRouter mocks base method. +func (m *MockNetworkClient) UpdateRouter(id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateRouter", id, opts) + ret0, _ := ret[0].(*routers.Router) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateRouter indicates an expected call of UpdateRouter. +func (mr *MockNetworkClientMockRecorder) UpdateRouter(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateRouter", reflect.TypeOf((*MockNetworkClient)(nil).UpdateRouter), id, opts) +} + +// UpdateSecGroup mocks base method. +func (m *MockNetworkClient) UpdateSecGroup(id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSecGroup", id, opts) + ret0, _ := ret[0].(*groups.SecGroup) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateSecGroup indicates an expected call of UpdateSecGroup. +func (mr *MockNetworkClientMockRecorder) UpdateSecGroup(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSecGroup", reflect.TypeOf((*MockNetworkClient)(nil).UpdateSecGroup), id, opts) +} + +// UpdateSubnet mocks base method. +func (m *MockNetworkClient) UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateSubnet", id, opts) + ret0, _ := ret[0].(*subnets.Subnet) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateSubnet indicates an expected call of UpdateSubnet. +func (mr *MockNetworkClientMockRecorder) UpdateSubnet(id, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubnet", reflect.TypeOf((*MockNetworkClient)(nil).UpdateSubnet), id, opts) +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/volume.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/volume.go new file mode 100644 index 000000000..6f71b2bbc --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock/volume.go @@ -0,0 +1,115 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by MockGen. DO NOT EDIT. +// Source: sigs.k8s.io/cluster-api-provider-openstack/pkg/clients (interfaces: VolumeClient) +// +// Generated by this command: +// +// mockgen -package mock -destination=volume.go sigs.k8s.io/cluster-api-provider-openstack/pkg/clients VolumeClient +// + +// Package mock is a generated GoMock package. +package mock + +import ( + reflect "reflect" + + volumes "github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes" + gomock "go.uber.org/mock/gomock" +) + +// MockVolumeClient is a mock of VolumeClient interface. +type MockVolumeClient struct { + ctrl *gomock.Controller + recorder *MockVolumeClientMockRecorder + isgomock struct{} +} + +// MockVolumeClientMockRecorder is the mock recorder for MockVolumeClient. +type MockVolumeClientMockRecorder struct { + mock *MockVolumeClient +} + +// NewMockVolumeClient creates a new mock instance. +func NewMockVolumeClient(ctrl *gomock.Controller) *MockVolumeClient { + mock := &MockVolumeClient{ctrl: ctrl} + mock.recorder = &MockVolumeClientMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockVolumeClient) EXPECT() *MockVolumeClientMockRecorder { + return m.recorder +} + +// CreateVolume mocks base method. +func (m *MockVolumeClient) CreateVolume(opts volumes.CreateOptsBuilder) (*volumes.Volume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateVolume", opts) + ret0, _ := ret[0].(*volumes.Volume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateVolume indicates an expected call of CreateVolume. +func (mr *MockVolumeClientMockRecorder) CreateVolume(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateVolume", reflect.TypeOf((*MockVolumeClient)(nil).CreateVolume), opts) +} + +// DeleteVolume mocks base method. +func (m *MockVolumeClient) DeleteVolume(volumeID string, opts volumes.DeleteOptsBuilder) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteVolume", volumeID, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteVolume indicates an expected call of DeleteVolume. +func (mr *MockVolumeClientMockRecorder) DeleteVolume(volumeID, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteVolume", reflect.TypeOf((*MockVolumeClient)(nil).DeleteVolume), volumeID, opts) +} + +// GetVolume mocks base method. +func (m *MockVolumeClient) GetVolume(volumeID string) (*volumes.Volume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetVolume", volumeID) + ret0, _ := ret[0].(*volumes.Volume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetVolume indicates an expected call of GetVolume. +func (mr *MockVolumeClientMockRecorder) GetVolume(volumeID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVolume", reflect.TypeOf((*MockVolumeClient)(nil).GetVolume), volumeID) +} + +// ListVolumes mocks base method. +func (m *MockVolumeClient) ListVolumes(opts volumes.ListOptsBuilder) ([]volumes.Volume, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListVolumes", opts) + ret0, _ := ret[0].([]volumes.Volume) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListVolumes indicates an expected call of ListVolumes. +func (mr *MockVolumeClientMockRecorder) ListVolumes(opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListVolumes", reflect.TypeOf((*MockVolumeClient)(nil).ListVolumes), opts) +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/networking.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/networking.go new file mode 100644 index 000000000..c9e111b3a --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/networking.go @@ -0,0 +1,466 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/attributestags" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/floatingips" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/groups" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/security/rules" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/trunks" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/ports" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" +) + +type NetworkClient interface { + ListFloatingIP(opts floatingips.ListOptsBuilder) ([]floatingips.FloatingIP, error) + CreateFloatingIP(opts floatingips.CreateOptsBuilder) (*floatingips.FloatingIP, error) + DeleteFloatingIP(id string) error + GetFloatingIP(id string) (*floatingips.FloatingIP, error) + UpdateFloatingIP(id string, opts floatingips.UpdateOptsBuilder) (*floatingips.FloatingIP, error) + + ListPort(opts ports.ListOptsBuilder) ([]ports.Port, error) + CreatePort(opts ports.CreateOptsBuilder) (*ports.Port, error) + DeletePort(id string) error + GetPort(id string) (*ports.Port, error) + UpdatePort(id string, opts ports.UpdateOptsBuilder) (*ports.Port, error) + + ListTrunk(opts trunks.ListOptsBuilder) ([]trunks.Trunk, error) + CreateTrunk(opts trunks.CreateOptsBuilder) (*trunks.Trunk, error) + DeleteTrunk(id string) error + + ListTrunkSubports(trunkID string) ([]trunks.Subport, error) + RemoveSubports(id string, opts trunks.RemoveSubportsOpts) error + + ListRouter(opts routers.ListOpts) ([]routers.Router, error) + CreateRouter(opts routers.CreateOptsBuilder) (*routers.Router, error) + DeleteRouter(id string) error + GetRouter(id string) (*routers.Router, error) + UpdateRouter(id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) + AddRouterInterface(id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) + RemoveRouterInterface(id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) + + ListSecGroup(opts groups.ListOpts) ([]groups.SecGroup, error) + CreateSecGroup(opts groups.CreateOptsBuilder) (*groups.SecGroup, error) + DeleteSecGroup(id string) error + GetSecGroup(id string) (*groups.SecGroup, error) + UpdateSecGroup(id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) + + ListSecGroupRule(opts rules.ListOpts) ([]rules.SecGroupRule, error) + CreateSecGroupRule(opts rules.CreateOptsBuilder) (*rules.SecGroupRule, error) + DeleteSecGroupRule(id string) error + GetSecGroupRule(id string) (*rules.SecGroupRule, error) + + ListNetwork(opts networks.ListOptsBuilder) ([]networks.Network, error) + CreateNetwork(opts networks.CreateOptsBuilder) (*networks.Network, error) + DeleteNetwork(id string) error + GetNetwork(id string) (*networks.Network, error) + UpdateNetwork(id string, opts networks.UpdateOptsBuilder) (*networks.Network, error) + + ListSubnet(opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) + CreateSubnet(opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) + DeleteSubnet(id string) error + GetSubnet(id string) (*subnets.Subnet, error) + UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) + + ListExtensions() ([]extensions.Extension, error) + + ReplaceAllAttributesTags(resourceType string, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) +} + +type networkClient struct { + serviceClient *gophercloud.ServiceClient +} + +// NewNetworkClient returns an instance of the networking service. +func NewNetworkClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (NetworkClient, error) { + serviceClient, err := openstack.NewNetworkV2(providerClient, gophercloud.EndpointOpts{ + Region: providerClientOpts.RegionName, + Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType), + }) + if err != nil { + return nil, fmt.Errorf("failed to create networking service providerClient: %v", err) + } + + return networkClient{serviceClient}, nil +} + +func (c networkClient) AddRouterInterface(id string, opts routers.AddInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { + mc := metrics.NewMetricPrometheusContext("server_os_interface", "create") + interfaceInfo, err := routers.AddInterface(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return interfaceInfo, nil +} + +func (c networkClient) RemoveRouterInterface(id string, opts routers.RemoveInterfaceOptsBuilder) (*routers.InterfaceInfo, error) { + mc := metrics.NewMetricPrometheusContext("server_os_interface", "delete") + interfaceInfo, err := routers.RemoveInterface(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return interfaceInfo, nil +} + +func (c networkClient) ReplaceAllAttributesTags(resourceType string, resourceID string, opts attributestags.ReplaceAllOptsBuilder) ([]string, error) { + mc := metrics.NewMetricPrometheusContext("attributes_tags", "replace_all") + tags, err := attributestags.ReplaceAll(context.TODO(), c.serviceClient, resourceType, resourceID, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return tags, nil +} + +func (c networkClient) ListRouter(opts routers.ListOpts) ([]routers.Router, error) { + mc := metrics.NewMetricPrometheusContext("router", "list") + allPages, err := routers.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return routers.ExtractRouters(allPages) +} + +func (c networkClient) ListFloatingIP(opts floatingips.ListOptsBuilder) ([]floatingips.FloatingIP, error) { + mc := metrics.NewMetricPrometheusContext("floating_ip", "list") + allPages, err := floatingips.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return floatingips.ExtractFloatingIPs(allPages) +} + +func (c networkClient) CreateFloatingIP(opts floatingips.CreateOptsBuilder) (*floatingips.FloatingIP, error) { + mc := metrics.NewMetricPrometheusContext("floating_ip", "create") + fip, err := floatingips.Create(context.TODO(), c.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return fip, nil +} + +func (c networkClient) DeleteFloatingIP(id string) error { + mc := metrics.NewMetricPrometheusContext("floating_ip", "delete") + return mc.ObserveRequestIgnoreNotFound(floatingips.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) GetFloatingIP(id string) (*floatingips.FloatingIP, error) { + mc := metrics.NewMetricPrometheusContext("floating_ip", "list") + fip, err := floatingips.Get(context.TODO(), c.serviceClient, id).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return fip, nil +} + +func (c networkClient) UpdateFloatingIP(id string, opts floatingips.UpdateOptsBuilder) (*floatingips.FloatingIP, error) { + mc := metrics.NewMetricPrometheusContext("floating_ip", "update") + fip, err := floatingips.Update(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return fip, nil +} + +func (c networkClient) ListPort(opts ports.ListOptsBuilder) ([]ports.Port, error) { + mc := metrics.NewMetricPrometheusContext("port", "list") + allPages, err := ports.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return ports.ExtractPorts(allPages) +} + +func (c networkClient) CreatePort(opts ports.CreateOptsBuilder) (*ports.Port, error) { + mc := metrics.NewMetricPrometheusContext("port", "create") + port, err := ports.Create(context.TODO(), c.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return port, nil +} + +func (c networkClient) DeletePort(id string) error { + mc := metrics.NewMetricPrometheusContext("port", "delete") + return mc.ObserveRequestIgnoreNotFound(ports.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) GetPort(id string) (*ports.Port, error) { + mc := metrics.NewMetricPrometheusContext("port", "get") + port, err := ports.Get(context.TODO(), c.serviceClient, id).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return port, nil +} + +func (c networkClient) UpdatePort(id string, opts ports.UpdateOptsBuilder) (*ports.Port, error) { + mc := metrics.NewMetricPrometheusContext("port", "update") + port, err := ports.Update(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return port, nil +} + +func (c networkClient) CreateTrunk(opts trunks.CreateOptsBuilder) (*trunks.Trunk, error) { + mc := metrics.NewMetricPrometheusContext("trunk", "create") + trunk, err := trunks.Create(context.TODO(), c.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return trunk, nil +} + +func (c networkClient) DeleteTrunk(id string) error { + mc := metrics.NewMetricPrometheusContext("trunk", "delete") + return mc.ObserveRequestIgnoreNotFound(trunks.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) ListTrunkSubports(trunkID string) ([]trunks.Subport, error) { + mc := metrics.NewMetricPrometheusContext("trunk", "listsubports") + subports, err := trunks.GetSubports(context.TODO(), c.serviceClient, trunkID).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return subports, nil +} + +func (c networkClient) RemoveSubports(id string, opts trunks.RemoveSubportsOpts) error { + mc := metrics.NewMetricPrometheusContext("trunk", "deletesubports") + _, err := trunks.RemoveSubports(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return err + } + return nil +} + +func (c networkClient) ListTrunk(opts trunks.ListOptsBuilder) ([]trunks.Trunk, error) { + mc := metrics.NewMetricPrometheusContext("trunk", "list") + allPages, err := trunks.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return trunks.ExtractTrunks(allPages) +} + +func (c networkClient) CreateRouter(opts routers.CreateOptsBuilder) (*routers.Router, error) { + mc := metrics.NewMetricPrometheusContext("router", "create") + router, err := routers.Create(context.TODO(), c.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return router, nil +} + +func (c networkClient) DeleteRouter(id string) error { + mc := metrics.NewMetricPrometheusContext("router", "delete") + return mc.ObserveRequestIgnoreNotFound(routers.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) GetRouter(id string) (*routers.Router, error) { + mc := metrics.NewMetricPrometheusContext("router", "get") + router, err := routers.Get(context.TODO(), c.serviceClient, id).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return router, nil +} + +func (c networkClient) UpdateRouter(id string, opts routers.UpdateOptsBuilder) (*routers.Router, error) { + mc := metrics.NewMetricPrometheusContext("router", "update") + router, err := routers.Update(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return router, nil +} + +func (c networkClient) ListSecGroup(opts groups.ListOpts) ([]groups.SecGroup, error) { + mc := metrics.NewMetricPrometheusContext("group", "list") + allPages, err := groups.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return groups.ExtractGroups(allPages) +} + +func (c networkClient) CreateSecGroup(opts groups.CreateOptsBuilder) (*groups.SecGroup, error) { + mc := metrics.NewMetricPrometheusContext("security_group", "create") + group, err := groups.Create(context.TODO(), c.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return group, nil +} + +func (c networkClient) DeleteSecGroup(id string) error { + mc := metrics.NewMetricPrometheusContext("security_group", "delete") + return mc.ObserveRequestIgnoreNotFound(groups.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) GetSecGroup(id string) (*groups.SecGroup, error) { + mc := metrics.NewMetricPrometheusContext("security_group", "get") + group, err := groups.Get(context.TODO(), c.serviceClient, id).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return group, nil +} + +func (c networkClient) UpdateSecGroup(id string, opts groups.UpdateOptsBuilder) (*groups.SecGroup, error) { + mc := metrics.NewMetricPrometheusContext("security_group", "update") + group, err := groups.Update(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return group, nil +} + +func (c networkClient) ListSecGroupRule(opts rules.ListOpts) ([]rules.SecGroupRule, error) { + mc := metrics.NewMetricPrometheusContext("security_group_rule", "list") + allPages, err := rules.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return rules.ExtractRules(allPages) +} + +func (c networkClient) CreateSecGroupRule(opts rules.CreateOptsBuilder) (*rules.SecGroupRule, error) { + mc := metrics.NewMetricPrometheusContext("security_group_rule", "create") + rule, err := rules.Create(context.TODO(), c.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return rule, nil +} + +func (c networkClient) DeleteSecGroupRule(id string) error { + mc := metrics.NewMetricPrometheusContext("security_group_rule", "delete") + return mc.ObserveRequestIgnoreNotFound(rules.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) GetSecGroupRule(id string) (*rules.SecGroupRule, error) { + mc := metrics.NewMetricPrometheusContext("security_group_rule", "get") + rule, err := rules.Get(context.TODO(), c.serviceClient, id).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return rule, nil +} + +func (c networkClient) ListNetwork(opts networks.ListOptsBuilder) ([]networks.Network, error) { + mc := metrics.NewMetricPrometheusContext("network", "list") + allPages, err := networks.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return networks.ExtractNetworks(allPages) +} + +func (c networkClient) CreateNetwork(opts networks.CreateOptsBuilder) (*networks.Network, error) { + mc := metrics.NewMetricPrometheusContext("network", "create") + net, err := networks.Create(context.TODO(), c.serviceClient, opts).Extract() + if (mc.ObserveRequest(err)) != nil { + return nil, err + } + return net, nil +} + +func (c networkClient) DeleteNetwork(id string) error { + mc := metrics.NewMetricPrometheusContext("network", "delete") + return mc.ObserveRequestIgnoreNotFound(networks.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) GetNetwork(id string) (*networks.Network, error) { + mc := metrics.NewMetricPrometheusContext("network", "get") + net, err := networks.Get(context.TODO(), c.serviceClient, id).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return net, nil +} + +func (c networkClient) UpdateNetwork(id string, opts networks.UpdateOptsBuilder) (*networks.Network, error) { + mc := metrics.NewMetricPrometheusContext("network", "update") + net, err := networks.Update(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return net, nil +} + +func (c networkClient) ListSubnet(opts subnets.ListOptsBuilder) ([]subnets.Subnet, error) { + mc := metrics.NewMetricPrometheusContext("subnet", "list") + allPages, err := subnets.List(c.serviceClient, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return subnets.ExtractSubnets(allPages) +} + +func (c networkClient) CreateSubnet(opts subnets.CreateOptsBuilder) (*subnets.Subnet, error) { + mc := metrics.NewMetricPrometheusContext("subnet", "create") + subnet, err := subnets.Create(context.TODO(), c.serviceClient, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return subnet, nil +} + +func (c networkClient) DeleteSubnet(id string) error { + mc := metrics.NewMetricPrometheusContext("subnet", "delete") + return mc.ObserveRequestIgnoreNotFound(subnets.Delete(context.TODO(), c.serviceClient, id).ExtractErr()) +} + +func (c networkClient) GetSubnet(id string) (*subnets.Subnet, error) { + mc := metrics.NewMetricPrometheusContext("subnet", "get") + subnet, err := subnets.Get(context.TODO(), c.serviceClient, id).Extract() + if mc.ObserveRequestIgnoreNotFound(err) != nil { + return nil, err + } + return subnet, nil +} + +func (c networkClient) UpdateSubnet(id string, opts subnets.UpdateOptsBuilder) (*subnets.Subnet, error) { + mc := metrics.NewMetricPrometheusContext("subnet", "update") + subnet, err := subnets.Update(context.TODO(), c.serviceClient, id, opts).Extract() + if mc.ObserveRequest(err) != nil { + return nil, err + } + return subnet, nil +} + +func (c networkClient) ListExtensions() ([]extensions.Extension, error) { + mc := metrics.NewMetricPrometheusContext("network_extension", "list") + allPages, err := extensions.List(c.serviceClient).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return extensions.ExtractExtensions(allPages) +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/volume.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/volume.go new file mode 100644 index 000000000..a9e89aec6 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/volume.go @@ -0,0 +1,101 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "context" + "fmt" + + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/blockstorage/v3/volumes" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + + "sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics" +) + +type VolumeClient interface { + ListVolumes(opts volumes.ListOptsBuilder) ([]volumes.Volume, error) + CreateVolume(opts volumes.CreateOptsBuilder) (*volumes.Volume, error) + DeleteVolume(volumeID string, opts volumes.DeleteOptsBuilder) error + GetVolume(volumeID string) (*volumes.Volume, error) +} + +type volumeClient struct{ client *gophercloud.ServiceClient } + +// NewVolumeClient returns a new cinder client. +func NewVolumeClient(providerClient *gophercloud.ProviderClient, providerClientOpts *clientconfig.ClientOpts) (VolumeClient, error) { + volume, err := openstack.NewBlockStorageV3(providerClient, gophercloud.EndpointOpts{ + Region: providerClientOpts.RegionName, + Availability: clientconfig.GetEndpointType(providerClientOpts.EndpointType), + }) + if err != nil { + return nil, fmt.Errorf("failed to create volume service client: %v", err) + } + + return &volumeClient{volume}, nil +} + +func (c volumeClient) ListVolumes(opts volumes.ListOptsBuilder) ([]volumes.Volume, error) { + mc := metrics.NewMetricPrometheusContext("volume", "list") + pages, err := volumes.List(c.client, opts).AllPages(context.TODO()) + if mc.ObserveRequest(err) != nil { + return nil, err + } + return volumes.ExtractVolumes(pages) +} + +func (c volumeClient) CreateVolume(opts volumes.CreateOptsBuilder) (*volumes.Volume, error) { + mc := metrics.NewMetricPrometheusContext("volume", "create") + volume, err := volumes.Create(context.TODO(), c.client, opts, nil).Extract() + return volume, mc.ObserveRequest(err) +} + +func (c volumeClient) DeleteVolume(volumeID string, opts volumes.DeleteOptsBuilder) error { + mc := metrics.NewMetricPrometheusContext("volume", "delete") + err := volumes.Delete(context.TODO(), c.client, volumeID, opts).ExtractErr() + return mc.ObserveRequestIgnoreNotFound(err) +} + +func (c volumeClient) GetVolume(volumeID string) (*volumes.Volume, error) { + mc := metrics.NewMetricPrometheusContext("volume", "get") + volume, err := volumes.Get(context.TODO(), c.client, volumeID).Extract() + return volume, mc.ObserveRequestIgnoreNotFound(err) +} + +type volumeErrorClient struct{ error } + +// NewVolumeErrorClient returns a VolumeClient in which every method returns the given error. +func NewVolumeErrorClient(e error) VolumeClient { + return volumeErrorClient{e} +} + +func (e volumeErrorClient) ListVolumes(_ volumes.ListOptsBuilder) ([]volumes.Volume, error) { + return nil, e.error +} + +func (e volumeErrorClient) CreateVolume(_ volumes.CreateOptsBuilder) (*volumes.Volume, error) { + return nil, e.error +} + +func (e volumeErrorClient) DeleteVolume(_ string, _ volumes.DeleteOptsBuilder) error { + return e.error +} + +func (e volumeErrorClient) GetVolume(_ string) (*volumes.Volume, error) { + return nil, e.error +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics/metrics.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics/metrics.go new file mode 100644 index 000000000..7bbeffff8 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/metrics/metrics.go @@ -0,0 +1,122 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package metrics + +import ( + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "sigs.k8s.io/controller-runtime/pkg/metrics" + + capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors" +) + +type OpenstackPrometheusMetrics struct { + Duration *prometheus.HistogramVec + Total *prometheus.CounterVec + Errors *prometheus.CounterVec +} + +// MetricPrometheusContext indicates the context for OpenStack metrics. +type MetricPrometheusContext struct { + Start time.Time + Attributes []string + Metrics *OpenstackPrometheusMetrics +} + +// NewMetricPrometheusContext creates a new MetricContext. +func NewMetricPrometheusContext(resource string, request string) *MetricPrometheusContext { + return &MetricPrometheusContext{ + Start: time.Now(), + Attributes: []string{resource + "_" + request}, + } +} + +// ObserveRequest records the request latency and counts the errors. +func (mc *MetricPrometheusContext) ObserveRequest(err error) error { + return mc.Observe(apiRequestPrometheusMetrics, err) +} + +// ObserveRequestIgnoreNotFound records the request latency and counts the errors if it's not IsNotFound. +func (mc *MetricPrometheusContext) ObserveRequestIgnoreNotFound(err error) error { + if capoerrors.IsNotFound(err) { + _ = mc.ObserveRequest(nil) + return err + } + return mc.ObserveRequest(err) +} + +// ObserveRequestIgnoreNotFoundorConflict records the request latency and counts the errors if it's not IsNotFound or IsConflict. +func (mc *MetricPrometheusContext) ObserveRequestIgnoreNotFoundorConflict(err error) error { + if capoerrors.IsNotFound(err) { + _ = mc.ObserveRequest(nil) + return err + } + if capoerrors.IsConflict(err) { + _ = mc.ObserveRequest(nil) + return err + } + return mc.ObserveRequest(err) +} + +// Observe records the request latency and counts the errors. +func (mc *MetricPrometheusContext) Observe(om *OpenstackPrometheusMetrics, err error) error { + if om == nil { + // mc.RequestMetrics not set, ignore this request + return err + } + + om.Duration.WithLabelValues(mc.Attributes...).Observe( + time.Since(mc.Start).Seconds()) + om.Total.WithLabelValues(mc.Attributes...).Inc() + if err != nil { + om.Errors.WithLabelValues(mc.Attributes...).Inc() + } + return err +} + +var apiRequestPrometheusMetrics = &OpenstackPrometheusMetrics{ + Duration: prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "capo", + Name: "openstack_api_request_duration_seconds", + Help: "Latency of an OpenStack API call", + }, []string{"request"}), + Total: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "capo", + Name: "openstack_api_requests_total", + Help: "Total number of OpenStack API calls", + }, []string{"request"}), + Errors: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "capo", + Name: "openstack_api_request_errors_total", + Help: "Total number of errors for an OpenStack API call", + }, []string{"request"}), +} + +var registerAPIPrometheusMetrics sync.Once + +func RegisterAPIPrometheusMetrics() { + registerAPIPrometheusMetrics.Do(func() { + metrics.Registry.MustRegister(apiRequestPrometheusMetrics.Duration) + metrics.Registry.MustRegister(apiRequestPrometheusMetrics.Total) + metrics.Registry.MustRegister(apiRequestPrometheusMetrics.Errors) + }) +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/hash.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/hash.go new file mode 100644 index 000000000..6f74902ab --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/hash.go @@ -0,0 +1,52 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scope + +import ( + "fmt" + "hash" + "hash/fnv" + + "github.com/davecgh/go-spew/spew" +) + +// spewHashObject writes specified object to hash using the spew library +// which follows pointers and prints actual values of the nested objects +// ensuring the hash does not change when a pointer changes. +func spewHashObject(hasher hash.Hash, objectToWrite interface{}) error { + hasher.Reset() + printer := spew.ConfigState{ + Indent: " ", + SortKeys: true, + DisableMethods: true, + SpewKeys: true, + } + + if _, err := printer.Fprintf(hasher, "%#v", objectToWrite); err != nil { + return fmt.Errorf("failed to write object to hasher") + } + return nil +} + +// computeSpewHash computes the hash of an object using the spew library. +func computeSpewHash(objectToWrite interface{}) (uint32, error) { + instanceSpecHasher := fnv.New32a() + if err := spewHashObject(instanceSpecHasher, objectToWrite); err != nil { + return 0, err + } + return instanceSpecHasher.Sum32(), nil +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/mock.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/mock.go new file mode 100644 index 000000000..d9568d644 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/mock.go @@ -0,0 +1,100 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scope + +import ( + "context" + "time" + + "github.com/go-logr/logr" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + "go.uber.org/mock/gomock" + "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients/mock" +) + +// MockScopeFactory implements both the ScopeFactory and ClientScope interfaces. It can be used in place of the default ProviderScopeFactory +// when we want to use mocked service clients which do not attempt to connect to a running OpenStack cloud. +type MockScopeFactory struct { + ComputeClient *mock.MockComputeClient + NetworkClient *mock.MockNetworkClient + VolumeClient *mock.MockVolumeClient + ImageClient *mock.MockImageClient + LbClient *mock.MockLbClient + + projectID string + clientScopeCreateError error +} + +func NewMockScopeFactory(mockCtrl *gomock.Controller, projectID string) *MockScopeFactory { + computeClient := mock.NewMockComputeClient(mockCtrl) + volumeClient := mock.NewMockVolumeClient(mockCtrl) + imageClient := mock.NewMockImageClient(mockCtrl) + networkClient := mock.NewMockNetworkClient(mockCtrl) + lbClient := mock.NewMockLbClient(mockCtrl) + + return &MockScopeFactory{ + ComputeClient: computeClient, + VolumeClient: volumeClient, + ImageClient: imageClient, + NetworkClient: networkClient, + LbClient: lbClient, + projectID: projectID, + } +} + +func (f *MockScopeFactory) SetClientScopeCreateError(err error) { + f.clientScopeCreateError = err +} + +func (f *MockScopeFactory) NewClientScopeFromObject(_ context.Context, _ client.Client, _ []byte, _ logr.Logger, _ ...infrav1.IdentityRefProvider) (Scope, error) { + if f.clientScopeCreateError != nil { + return nil, f.clientScopeCreateError + } + return f, nil +} + +func (f *MockScopeFactory) NewComputeClient() (clients.ComputeClient, error) { + return f.ComputeClient, nil +} + +func (f *MockScopeFactory) NewVolumeClient() (clients.VolumeClient, error) { + return f.VolumeClient, nil +} + +func (f *MockScopeFactory) NewImageClient() (clients.ImageClient, error) { + return f.ImageClient, nil +} + +func (f *MockScopeFactory) NewNetworkClient() (clients.NetworkClient, error) { + return f.NetworkClient, nil +} + +func (f *MockScopeFactory) NewLbClient() (clients.LbClient, error) { + return f.LbClient, nil +} + +func (f *MockScopeFactory) ProjectID() string { + return f.projectID +} + +func (f *MockScopeFactory) ExtractToken() (*tokens.Token, error) { + return &tokens.Token{ExpiresAt: time.Now().Add(24 * time.Hour)}, nil +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/provider.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/provider.go new file mode 100644 index 000000000..d23e3323a --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/provider.go @@ -0,0 +1,309 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scope + +import ( + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net/http" + "time" + + "github.com/go-logr/logr" + "github.com/gophercloud/gophercloud/v2" + "github.com/gophercloud/gophercloud/v2/openstack" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + osclient "github.com/gophercloud/utils/v2/client" + "github.com/gophercloud/utils/v2/openstack/clientconfig" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/cache" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/yaml" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients" + "sigs.k8s.io/cluster-api-provider-openstack/version" +) + +const ( + CloudsSecretKey = "clouds.yaml" + CASecretKey = "cacert" +) + +type providerScopeFactory struct { + clientCache *cache.LRUExpireCache +} + +func (f *providerScopeFactory) NewClientScopeFromObject(ctx context.Context, ctrlClient client.Client, defaultCACert []byte, logger logr.Logger, objects ...infrav1.IdentityRefProvider) (Scope, error) { + var namespace *string + var identityRef *infrav1.OpenStackIdentityReference + + for _, o := range objects { + namespace, identityRef = o.GetIdentityRef() + if namespace != nil || identityRef != nil { + break + } + } + + if namespace == nil || identityRef == nil { + return nil, fmt.Errorf("unable to get identityRef from provided objects") + } + + var cloud clientconfig.Cloud + var caCert []byte + + var err error + cloud, caCert, err = getCloudFromSecret(ctx, ctrlClient, *namespace, identityRef.Name, identityRef.CloudName) + if err != nil { + return nil, err + } + + if caCert == nil { + caCert = defaultCACert + } + + if f.clientCache == nil { + return NewProviderScope(cloud, identityRef.Region, caCert, logger) + } + + return NewCachedProviderScope(f.clientCache, cloud, identityRef.Region, caCert, logger) +} + +func getScopeCacheKey(cloud clientconfig.Cloud) (string, error) { + key, err := computeSpewHash(cloud) + if err != nil { + return "", err + } + + return fmt.Sprintf("%d", key), nil +} + +type providerScope struct { + providerClient *gophercloud.ProviderClient + providerClientOpts *clientconfig.ClientOpts + projectID string +} + +func NewProviderScope(cloud clientconfig.Cloud, regionName string, caCert []byte, logger logr.Logger) (Scope, error) { + providerClient, clientOpts, projectID, err := NewProviderClient(cloud, regionName, caCert, logger) + if err != nil { + return nil, err + } + + return &providerScope{ + providerClient: providerClient, + providerClientOpts: clientOpts, + projectID: projectID, + }, nil +} + +func NewCachedProviderScope(cache *cache.LRUExpireCache, cloud clientconfig.Cloud, regionName string, caCert []byte, logger logr.Logger) (Scope, error) { + key, err := getScopeCacheKey(cloud) + if err != nil { + return nil, fmt.Errorf("compute cloud config cache key: %w", err) + } + + if scope, found := cache.Get(key); found { + logger.V(6).Info("Using scope from cache") + return scope.(Scope), nil + } + + scope, err := NewProviderScope(cloud, regionName, caCert, logger) + if err != nil { + return nil, err + } + + token, err := scope.ExtractToken() + if err != nil { + return nil, err + } + + // compute the token expiration time + expiry := time.Until(token.ExpiresAt) / 2 + + cache.Add(key, scope, expiry) + return scope, nil +} + +func (s *providerScope) ProjectID() string { + return s.projectID +} + +func (s *providerScope) NewComputeClient() (clients.ComputeClient, error) { + return clients.NewComputeClient(s.providerClient, s.providerClientOpts) +} + +func (s *providerScope) NewNetworkClient() (clients.NetworkClient, error) { + return clients.NewNetworkClient(s.providerClient, s.providerClientOpts) +} + +func (s *providerScope) NewVolumeClient() (clients.VolumeClient, error) { + return clients.NewVolumeClient(s.providerClient, s.providerClientOpts) +} + +func (s *providerScope) NewImageClient() (clients.ImageClient, error) { + return clients.NewImageClient(s.providerClient, s.providerClientOpts) +} + +func (s *providerScope) NewLbClient() (clients.LbClient, error) { + return clients.NewLbClient(s.providerClient, s.providerClientOpts) +} + +func (s *providerScope) ExtractToken() (*tokens.Token, error) { + client, err := openstack.NewIdentityV3(s.providerClient, gophercloud.EndpointOpts{}) + if err != nil { + return nil, fmt.Errorf("create new identity service client: %w", err) + } + return tokens.Get(context.TODO(), client, s.providerClient.Token()).ExtractToken() +} + +func NewProviderClient(cloud clientconfig.Cloud, regionName string, caCert []byte, logger logr.Logger) (*gophercloud.ProviderClient, *clientconfig.ClientOpts, string, error) { + clientOpts := new(clientconfig.ClientOpts) + + // We explicitly disable reading auth data from env variables by setting an invalid EnvPrefix. + // By doing this, we make sure that the data from clouds.yaml is enough to authenticate. + // For more information: https://github.com/gophercloud/utils/v2/blob/8677e053dcf1f05d0fa0a616094aace04690eb94/openstack/clientconfig/requests.go#L508 + clientOpts.EnvPrefix = "NO_ENV_VARIABLES_" + if regionName == "" { + regionName = cloud.RegionName + } + if cloud.AuthInfo != nil { + clientOpts.AuthInfo = cloud.AuthInfo + clientOpts.AuthType = cloud.AuthType + clientOpts.RegionName = regionName + clientOpts.EndpointType = cloud.EndpointType + } + + opts, err := clientconfig.AuthOptions(clientOpts) + if err != nil { + return nil, nil, "", fmt.Errorf("auth option failed for cloud %v: %v", cloud.Cloud, err) + } + opts.AllowReauth = true + + provider, err := openstack.NewClient(opts.IdentityEndpoint) + if err != nil { + return nil, nil, "", fmt.Errorf("create providerClient err: %v", err) + } + + ua := gophercloud.UserAgent{} + ua.Prepend(fmt.Sprintf("cluster-api-provider-openstack/%s", version.Get().String())) + provider.UserAgent = ua + + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + } + if cloud.Verify != nil { + config.InsecureSkipVerify = !*cloud.Verify + } + if caCert != nil { + config.RootCAs = x509.NewCertPool() + ok := config.RootCAs.AppendCertsFromPEM(caCert) + if !ok { + // If no certificates were successfully parsed, set RootCAs to nil to use the host's root CA + config.RootCAs = nil + } + } + + provider.HTTPClient.Transport = &http.Transport{Proxy: http.ProxyFromEnvironment, TLSClientConfig: config} + if klog.V(6).Enabled() { + provider.HTTPClient.Transport = &osclient.RoundTripper{ + Rt: provider.HTTPClient.Transport, + Logger: &gophercloudLogger{logger}, + } + } + err = openstack.Authenticate(context.TODO(), provider, *opts) + if err != nil { + return nil, nil, "", fmt.Errorf("providerClient authentication err: %v", err) + } + + projectID, err := getProjectIDFromAuthResult(provider.GetAuthResult()) + if err != nil { + return nil, nil, "", err + } + + return provider, clientOpts, projectID, nil +} + +type gophercloudLogger struct { + logger logr.Logger +} + +// Printf is a default Printf method. +func (g gophercloudLogger) Printf(format string, args ...interface{}) { + g.logger.Info(fmt.Sprintf(format, args...)) +} + +// getCloudFromSecret extract a Cloud from the given namespace:secretName. +func getCloudFromSecret(ctx context.Context, ctrlClient client.Client, secretNamespace string, secretName string, cloudName string) (clientconfig.Cloud, []byte, error) { + emptyCloud := clientconfig.Cloud{} + + if secretName == "" { + return emptyCloud, nil, nil + } + + if cloudName == "" { + return emptyCloud, nil, fmt.Errorf("secret name set to %v but no cloud was specified. Please set cloud_name in your machine spec", secretName) + } + + secret := &corev1.Secret{} + err := ctrlClient.Get(ctx, types.NamespacedName{ + Namespace: secretNamespace, + Name: secretName, + }, secret) + if err != nil { + return emptyCloud, nil, err + } + + content, ok := secret.Data[CloudsSecretKey] + if !ok { + return emptyCloud, nil, fmt.Errorf("OpenStack credentials secret %v did not contain key %v", + secretName, CloudsSecretKey) + } + var clouds clientconfig.Clouds + if err = yaml.Unmarshal(content, &clouds); err != nil { + return emptyCloud, nil, fmt.Errorf("failed to unmarshal clouds credentials stored in secret %v: %v", secretName, err) + } + + // get caCert + caCert, ok := secret.Data[CASecretKey] + if !ok { + return clouds.Clouds[cloudName], nil, nil + } + + return clouds.Clouds[cloudName], caCert, nil +} + +// getProjectIDFromAuthResult handles different auth mechanisms to retrieve the +// current project id. Usually we use the Identity v3 Token mechanism that +// returns the project id in the response to the initial auth request. +func getProjectIDFromAuthResult(authResult gophercloud.AuthResult) (string, error) { + switch authResult := authResult.(type) { + case tokens.CreateResult: + project, err := authResult.ExtractProject() + if err != nil { + return "", fmt.Errorf("unable to extract project from CreateResult: %v", err) + } + + return project.ID, nil + + default: + return "", fmt.Errorf("unable to get the project id from auth response with type %T", authResult) + } +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/scope.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/scope.go new file mode 100644 index 000000000..93f070fe7 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/scope/scope.go @@ -0,0 +1,75 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scope + +import ( + "context" + + "github.com/go-logr/logr" + "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/tokens" + "k8s.io/apimachinery/pkg/util/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + + infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + "sigs.k8s.io/cluster-api-provider-openstack/pkg/clients" +) + +// NewFactory creates the default scope factory. It generates service clients which make OpenStack API calls against a running cloud. +func NewFactory(maxCacheSize int) Factory { + var c *cache.LRUExpireCache + if maxCacheSize > 0 { + c = cache.NewLRUExpireCache(maxCacheSize) + } + return &providerScopeFactory{ + clientCache: c, + } +} + +// Factory instantiates a new Scope using credentials from an IdentityRefProvider. +type Factory interface { + // NewClientScopeFromObject creates a new scope from the first object which returns an OpenStackIdentityRef + NewClientScopeFromObject(ctx context.Context, ctrlClient client.Client, defaultCACert []byte, logger logr.Logger, objects ...infrav1.IdentityRefProvider) (Scope, error) +} + +// Scope contains arguments common to most operations. +type Scope interface { + NewComputeClient() (clients.ComputeClient, error) + NewVolumeClient() (clients.VolumeClient, error) + NewImageClient() (clients.ImageClient, error) + NewNetworkClient() (clients.NetworkClient, error) + NewLbClient() (clients.LbClient, error) + ProjectID() string + ExtractToken() (*tokens.Token, error) +} + +// WithLogger extends Scope with a logger. +type WithLogger struct { + Scope + + logger logr.Logger +} + +func NewWithLogger(scope Scope, logger logr.Logger) *WithLogger { + return &WithLogger{ + Scope: scope, + logger: logger, + } +} + +func (s *WithLogger) Logger() logr.Logger { + return s.logger +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack/loadbalancer.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack/loadbalancer.go new file mode 100644 index 000000000..37fea19da --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack/loadbalancer.go @@ -0,0 +1,81 @@ +/* +Copyright 2022 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openstack + +import ( + version "github.com/hashicorp/go-version" + klog "k8s.io/klog/v2" +) + +const ( + OctaviaFeatureTags = 0 + OctaviaFeatureVIPACL = 1 + OctaviaFeatureFlavors = 2 + OctaviaFeatureTimeout = 3 + OctaviaFeatureAvailabilityZones = 4 + lbProviderOVN = "ovn" +) + +// IsOctaviaFeatureSupported returns true if the given feature is supported in the deployed Octavia version. +// copied from https://github.com/kubernetes/cloud-provider-openstack/blob/master/pkg/util/openstack/loadbalancer.go#L95-L148 +func IsOctaviaFeatureSupported(octaviaVer string, feature int, lbProvider string) bool { + currentVer, _ := version.NewVersion(octaviaVer) + + switch feature { + case OctaviaFeatureVIPACL: + if lbProvider == lbProviderOVN { + return false + } + verACL, _ := version.NewVersion("v2.12") + if currentVer.GreaterThanOrEqual(verACL) { + return true + } + case OctaviaFeatureTags: + verTags, _ := version.NewVersion("v2.5") + if currentVer.GreaterThanOrEqual(verTags) { + return true + } + case OctaviaFeatureFlavors: + if lbProvider == lbProviderOVN { + return false + } + verFlavors, _ := version.NewVersion("v2.6") + if currentVer.GreaterThanOrEqual(verFlavors) { + return true + } + case OctaviaFeatureTimeout: + if lbProvider == lbProviderOVN { + return false + } + verFlavors, _ := version.NewVersion("v2.1") + if currentVer.GreaterThanOrEqual(verFlavors) { + return true + } + case OctaviaFeatureAvailabilityZones: + if lbProvider == lbProviderOVN { + return false + } + verAvailabilityZones, _ := version.NewVersion("v2.14") + if currentVer.GreaterThanOrEqual(verAvailabilityZones) { + return true + } + default: + klog.Warningf("Feature %d not recognized", feature) + } + + return false +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack/microversion.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack/microversion.go new file mode 100644 index 000000000..72478a40e --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/openstack/microversion.go @@ -0,0 +1,98 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package openstack + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/gophercloud/gophercloud/v2" +) + +// GetSupportedMicroversions returns the minimum and maximum microversion that is supported by the ServiceClient Endpoint. +func GetSupportedMicroversions(client gophercloud.ServiceClient) (string, string, error) { + type valueResp struct { + ID string `json:"id"` + Status string `json:"status"` + Version string `json:"version"` + MinVersion string `json:"min_version"` + } + + type response struct { + Version valueResp `json:"version"` + } + var resp response + _, err := client.Request(context.TODO(), "GET", client.Endpoint, &gophercloud.RequestOpts{ + JSONResponse: &resp, + OkCodes: []int{200, 300}, + }) + if err != nil { + return "", "", err + } + + return resp.Version.MinVersion, resp.Version.Version, nil +} + +// MicroversionSupported checks if a microversion falls in the supported interval. +// It returns true if the version is within the interval and false otherwise. +func MicroversionSupported(version string, minVersion string, maxVersion string) (bool, error) { + // Parse the version X.Y into X and Y integers that are easier to compare. + vMajor, v, err := parseMicroversion(version) + if err != nil { + return false, err + } + minMajor, minimum, err := parseMicroversion(minVersion) + if err != nil { + return false, err + } + maxMajor, maximum, err := parseMicroversion(maxVersion) + if err != nil { + return false, err + } + + // Check that the major version number is supported. + if (vMajor < minMajor) || (vMajor > maxMajor) { + return false, err + } + + // Check that the minor version number is supported + if (v <= maximum) && (v >= minimum) { + return true, nil + } + + return false, nil +} + +// parseMicroversion parses the version X.Y into separate integers X and Y. +// For example, "2.53" becomes 2 and 53. +func parseMicroversion(version string) (int, int, error) { + parts := strings.Split(version, ".") + if len(parts) != 2 { + return 0, 0, fmt.Errorf("invalid microversion format: %q", version) + } + major, err := strconv.Atoi(parts[0]) + if err != nil { + return 0, 0, err + } + minor, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, 0, err + } + return major, minor, nil +} diff --git a/vendor/sigs.k8s.io/cluster-api-provider-openstack/version/version.go b/vendor/sigs.k8s.io/cluster-api-provider-openstack/version/version.go new file mode 100644 index 000000000..ab2fd9749 --- /dev/null +++ b/vendor/sigs.k8s.io/cluster-api-provider-openstack/version/version.go @@ -0,0 +1,62 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package version + +import ( + "fmt" + "runtime" +) + +var ( + gitMajor string // major version, always numeric + gitMinor string // minor version, numeric possibly followed by "+" + gitVersion string // semantic version, derived by build scripts + gitCommit string // sha1 from git, output of $(git rev-parse HEAD) + gitTreeState string // state of git tree, either "clean" or "dirty" + buildDate string // build date in ISO8601 format, output of $(date -u +'%Y-%m-%dT%H:%M:%SZ') +) + +type Info struct { + Major string `json:"major,omitempty"` + Minor string `json:"minor,omitempty"` + GitVersion string `json:"gitVersion,omitempty"` + GitCommit string `json:"gitCommit,omitempty"` + GitTreeState string `json:"gitTreeState,omitempty"` + BuildDate string `json:"buildDate,omitempty"` + GoVersion string `json:"goVersion,omitempty"` + Compiler string `json:"compiler,omitempty"` + Platform string `json:"platform,omitempty"` +} + +func Get() Info { + return Info{ + Major: gitMajor, + Minor: gitMinor, + GitVersion: gitVersion, + GitCommit: gitCommit, + GitTreeState: gitTreeState, + BuildDate: buildDate, + GoVersion: runtime.Version(), + Compiler: runtime.Compiler, + Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH), + } +} + +// String returns info as a human-friendly version string. +func (info Info) String() string { + return info.GitVersion +}