diff --git a/go.mod b/go.mod index 8ea4fb4..fd349c8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/ftarasenko/go-openstackclient go 1.26.0 require ( - github.com/gophercloud/gophercloud/v2 v2.14.0 + github.com/gophercloud/gophercloud/v2 v2.15.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 golang.org/x/crypto v0.57.0 diff --git a/go.sum b/go.sum index a8a6eed..2bb107a 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/gophercloud/gophercloud/v2 v2.14.0 h1:xGxKCvyaOxJDc5FqrnKDNqtdYn43ocQPuJ2Cm4KX/cs= -github.com/gophercloud/gophercloud/v2 v2.14.0/go.mod h1:4fs5I9VH6Wg2LyocDL9xf0ASb8VD63tyLA8sgAX/69U= +github.com/gophercloud/gophercloud/v2 v2.15.0 h1:4zLiLYTFraZMlJ77FH1Kzq7itjfVP+BIbWcCurCrgic= +github.com/gophercloud/gophercloud/v2 v2.15.0/go.mod h1:4fs5I9VH6Wg2LyocDL9xf0ASb8VD63tyLA8sgAX/69U= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md b/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md index c3b535e..9bd4237 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md +++ b/vendor/github.com/gophercloud/gophercloud/v2/CHANGELOG.md @@ -1,3 +1,22 @@ +## v2.15.0 (2026-09-18) + +* [GH-3980](https://github.com/gophercloud/gophercloud/pull/3980) [v2] fix(config/clouds): resolve reference to clouds-public.yaml +* [GH-3992](https://github.com/gophercloud/gophercloud/pull/3992) [v2] test(db): use datastore-appropriate config parameter in TestConfigurationsCRUD +* [GH-3996](https://github.com/gophercloud/gophercloud/pull/3996) build(deps): bump the all-github-actions group with 3 updates +* [GH-4001](https://github.com/gophercloud/gophercloud/pull/4001) [v2] feat: add loadbalancer provider capabilities +* [GH-4002](https://github.com/gophercloud/gophercloud/pull/4002) build(deps): bump the all-github-actions group with 3 updates +* [GH-4004](https://github.com/gophercloud/gophercloud/pull/4004) [v2] auth: support more scope cases in clouds.yaml parser +* [GH-4007](https://github.com/gophercloud/gophercloud/pull/4007) [v2] collectlogs: fix regex to capture all devstack service names +* [GH-4011](https://github.com/gophercloud/gophercloud/pull/4011) [v2] baremetal: account for different types of CPU.Frequency +* [GH-4014](https://github.com/gophercloud/gophercloud/pull/4014) [v2] fix(params): allow zero values for required numeric fields +* [GH-4015](https://github.com/gophercloud/gophercloud/pull/4015) identity: credentials Update test sends only blob +* [GH-4018](https://github.com/gophercloud/gophercloud/pull/4018) [v2] Preserve integer precision when decoding JSON into any +* [GH-4019](https://github.com/gophercloud/gophercloud/pull/4019) [v2] reservation: add hosts resources +* [GH-4021](https://github.com/gophercloud/gophercloud/pull/4021) [v2] baremetal: also account for Frequency as an empty string +* [GH-4022](https://github.com/gophercloud/gophercloud/pull/4022) reservation: Add Blazar service client and CI job +* [GH-4023](https://github.com/gophercloud/gophercloud/pull/4023) [v2] identity: add RegionID to endpoints +* [GH-4024](https://github.com/gophercloud/gophercloud/pull/4024) [v2] Add CI debug skill for acceptance test failure analysis + ## v2.14.0 (2026-08-20) * [GH-3797](https://github.com/gophercloud/gophercloud/pull/3797) build(deps): bump github/codeql-action from 4.36.1 to 4.36.2 diff --git a/vendor/github.com/gophercloud/gophercloud/v2/Makefile b/vendor/github.com/gophercloud/gophercloud/v2/Makefile index 9041e0c..56281dc 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/Makefile +++ b/vendor/github.com/gophercloud/gophercloud/v2/Makefile @@ -43,7 +43,7 @@ coverage: $(GO_TEST) -shuffle on -covermode count -coverprofile cover.out -coverpkg=./... ./... .PHONY: coverage -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-metric 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-metric acceptance-networking acceptance-objectstorage acceptance-orchestration acceptance-placement acceptance-reservation acceptance-sharedfilesystems acceptance-workflow .PHONY: acceptance acceptance-basic: @@ -122,6 +122,10 @@ acceptance-placement: $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/placement/... .PHONY: acceptance-placement +acceptance-reservation: + $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/reservation/... +.PHONY: acceptance-reservation + acceptance-sharedfilesystems: $(GO_TEST) -timeout $(TIMEOUT) -tags "fixtures acceptance" ./internal/acceptance/openstack/sharedfilesystems/... .PHONY: acceptance-sharefilesystems diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/inventory/types.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/inventory/types.go index 98498d5..ee57083 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/inventory/types.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/inventory/types.go @@ -1,16 +1,23 @@ package inventory +import ( + "encoding/json" + "fmt" + "slices" +) + type BootInfoType struct { CurrentBootMode string `json:"current_boot_mode"` PXEInterface string `json:"pxe_interface"` } type CPUType struct { - Architecture string `json:"architecture"` - Count int `json:"count"` - Flags []string `json:"flags"` - Frequency string `json:"frequency"` - ModelName string `json:"model_name"` + Architecture string `json:"architecture"` + Count int `json:"count"` + Flags []string `json:"flags"` + Frequency string `json:"-"` + ModelName string `json:"model_name"` + RealFrequency json.Number `json:"frequency"` } type InterfaceType struct { @@ -69,3 +76,40 @@ type InventoryType struct { SystemVendor SystemVendorType `json:"system_vendor"` Hostname string `json:"hostname"` } + +func (inv *InventoryType) Compat() { + inv.CPU.Frequency = string(inv.CPU.RealFrequency) +} + +var emptyFrequency = []string{``, `""`, `null`} + +func (t *CPUType) UnmarshalJSON(data []byte) error { + if len(data) == 0 || string(data) == "null" { + return nil + } + + var cpuTypeCompat struct { + Architecture string `json:"architecture"` + Count int `json:"count"` + Flags []string `json:"flags"` + Frequency json.RawMessage `json:"frequency"` + ModelName string `json:"model_name"` + } + if err := json.Unmarshal(data, &cpuTypeCompat); err != nil { + return err + } + + // This is the main point of this function: frequency can be an empty string + if !slices.Contains(emptyFrequency, string(cpuTypeCompat.Frequency)) { + if err := json.Unmarshal(cpuTypeCompat.Frequency, &t.RealFrequency); err != nil { + return fmt.Errorf("unable to unmarshal frequency: %w", err) + } + t.Frequency = string(t.RealFrequency) + } + + t.Architecture = cpuTypeCompat.Architecture + t.Count = cpuTypeCompat.Count + t.Flags = cpuTypeCompat.Flags + t.ModelName = cpuTypeCompat.ModelName + return nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes/results.go index f62e16a..93160c9 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetal/v1/nodes/results.go @@ -657,6 +657,7 @@ func (pd PluginData) AsStandardData() (result inventory.StandardPluginData, err // AsInspectorData interprets plugin data as coming from ironic-inspector. func (pd PluginData) AsInspectorData() (result introspection.Data, err error) { err = json.Unmarshal(pd.RawMessage, &result) + result.Inventory.Compat() return } @@ -705,6 +706,7 @@ type InventoryResult struct { func (r InventoryResult) Extract() (*InventoryData, error) { var data InventoryData err := r.ExtractInto(&data) + data.Inventory.Compat() return &data, err } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetalintrospection/v1/introspection/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetalintrospection/v1/introspection/results.go index c774094..01f2f45 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetalintrospection/v1/introspection/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/baremetalintrospection/v1/introspection/results.go @@ -193,6 +193,7 @@ type BaseInterfaceType struct { func (r DataResult) Extract() (*Data, error) { var s Data err := r.ExtractInto(&s) + s.Inventory.Compat() return &s, err } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go index cd95e59..e1bfa3e 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/client.go @@ -508,3 +508,15 @@ func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOp func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { return initClientOpts(client, eo, "placement", 1) } + +// NewReservationV1 creates a ServiceClient that may be used with the reservation package. +func NewReservationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "reservation", 1) + if err != nil { + return sc, err + } + if !strings.HasSuffix(strings.TrimSuffix(sc.Endpoint, "/"), "v1") { + sc.ResourceBase = sc.Endpoint + "v1/" + } + return sc, nil +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/clouds.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/clouds.go index 73dd709..76c1e63 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/clouds.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/clouds.go @@ -81,15 +81,9 @@ func Parse(opts ...ParseOption) (gophercloud.AuthOptions, gophercloud.EndpointOp if err != nil { return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to get the current working directory: %w", err) } - // Use XDG_CONFIG_HOME or fall back to ~/.config, matching the - // OpenStack convention for clouds.yaml location on all platforms. - userConfig := os.Getenv("XDG_CONFIG_HOME") - if userConfig == "" { - homeDir, err := os.UserHomeDir() - if err != nil { - return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("failed to get the user home directory: %w", err) - } - userConfig = path.Join(homeDir, ".config") + userConfig, err := getUserConfig() + if err != nil { + return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, err } options.locations = []string{path.Join(cwd, "clouds.yaml"), path.Join(userConfig, "openstack", "clouds.yaml"), path.Join("/etc", "openstack", "clouds.yaml")} } @@ -141,12 +135,17 @@ func Parse(opts ...ParseOption) (gophercloud.AuthOptions, gophercloud.EndpointOp var err error cloud, err = mergeClouds(secureCloud, cloud) if err != nil { - return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml") + return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("unable to merge information from clouds.yaml and secure.yaml: %w", err) } } } } + cloud, err := mergeWithPublicClouds(cloud, &options) + if err != nil { + return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, err + } + tlsConfig, err := computeTLSConfig(cloud, options) if err != nil { return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("unable to compute TLS configuration: %w", err) @@ -154,20 +153,57 @@ func Parse(opts ...ParseOption) (gophercloud.AuthOptions, gophercloud.EndpointOp endpointType := coalesce(options.endpointType, cloud.EndpointType, cloud.Interface) + // Build the scope with proper project domain separation var scope *gophercloud.AuthScope if trustID := cloud.AuthInfo.TrustID; trustID != "" { scope = &gophercloud.AuthScope{ TrustID: trustID, } + } else if systemScope := cloud.AuthInfo.SystemScope; systemScope != "" { + // System scoping for admin operations + if systemScope != "all" { + return gophercloud.AuthOptions{}, gophercloud.EndpointOpts{}, nil, fmt.Errorf("only system scope of all is supported") + } + scope = &gophercloud.AuthScope{ + System: true, + } + } else if projectID := coalesce(options.projectID, cloud.AuthInfo.ProjectID); projectID != "" { + // Project scoping by ID + scope = &gophercloud.AuthScope{ + ProjectID: projectID, + } + } else if projectName := coalesce(options.projectName, cloud.AuthInfo.ProjectName); projectName != "" { + // Project scoping by name requires project domain + scope = &gophercloud.AuthScope{ + ProjectName: projectName, + // The follow UP PR will remove the fallback to + // DomainID and DomainName, and will require the user + // to specify the project domain explicitly. + DomainID: coalesce(cloud.AuthInfo.ProjectDomainID, cloud.AuthInfo.DomainID), + DomainName: coalesce(cloud.AuthInfo.ProjectDomainName, cloud.AuthInfo.DomainName), + } + } else if domainID := coalesce(options.domainID, cloud.AuthInfo.DomainID); domainID != "" { + // Domain scoping by ID (when no project is specified) + scope = &gophercloud.AuthScope{ + DomainID: domainID, + } + } else if domainName := coalesce(options.domainName, cloud.AuthInfo.DomainName); domainName != "" { + // Domain scoping by name (when no project is specified) + scope = &gophercloud.AuthScope{ + DomainName: domainName, + } } return gophercloud.AuthOptions{ - IdentityEndpoint: coalesce(options.authURL, cloud.AuthInfo.AuthURL), - Username: coalesce(options.username, cloud.AuthInfo.Username), - UserID: coalesce(options.userID, cloud.AuthInfo.UserID), - Password: coalesce(options.password, cloud.AuthInfo.Password), - DomainID: coalesce(options.domainID, cloud.AuthInfo.UserDomainID, cloud.AuthInfo.ProjectDomainID, cloud.AuthInfo.DomainID), - DomainName: coalesce(options.domainName, cloud.AuthInfo.UserDomainName, cloud.AuthInfo.ProjectDomainName, cloud.AuthInfo.DomainName), + IdentityEndpoint: coalesce(options.authURL, cloud.AuthInfo.AuthURL), + Username: coalesce(options.username, cloud.AuthInfo.Username), + UserID: coalesce(options.userID, cloud.AuthInfo.UserID), + Password: coalesce(options.password, cloud.AuthInfo.Password), + // The follow UP PR will remove the fallback to + // DomainID and DomainName, and will require the user + // to specify the project domain explicitly. + DomainID: coalesce(options.domainID, cloud.AuthInfo.UserDomainID, cloud.AuthInfo.DomainID), + DomainName: coalesce(options.domainName, cloud.AuthInfo.UserDomainName, cloud.AuthInfo.DomainName), TenantID: coalesce(options.projectID, cloud.AuthInfo.ProjectID), TenantName: coalesce(options.projectName, cloud.AuthInfo.ProjectName), TokenID: coalesce(options.token, cloud.AuthInfo.Token), @@ -183,6 +219,81 @@ func Parse(opts ...ParseOption) (gophercloud.AuthOptions, gophercloud.EndpointOp nil } +func getUserConfig() (string, error) { + // Use XDG_CONFIG_HOME or fall back to ~/.config, matching the + // OpenStack convention for clouds.yaml location on all platforms. + userConfig := os.Getenv("XDG_CONFIG_HOME") + if userConfig != "" { + return userConfig, nil + } + + homeDir, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get the user home directory: %w", err) + } + userConfig = path.Join(homeDir, ".config") + return userConfig, nil +} + +func mergeWithPublicClouds(cloud Cloud, options *cloudOpts) (Cloud, error) { + var mergeWith string + if cloud.Profile != "" { + mergeWith = cloud.Profile + } else if cloud.Cloud != "" { + mergeWith = cloud.Cloud + } else { + return cloud, nil + } + + // Code is executed only if cloud needs to be merged with a public + // profile, error are returned only loading clouds-public.yaml was + // needed + if options.cloudsPublicyamlReader == nil { + if len(options.publicLocations) < 1 { + cwd, err := os.Getwd() + if err != nil { + return cloud, fmt.Errorf("failed to get the current directory: %w", err) + } + userConfig, err := getUserConfig() + if err != nil { + return cloud, err + } + options.publicLocations = []string{path.Join(cwd, "clouds-public.yaml"), path.Join(userConfig, "openstack", "clouds-public.yaml"), path.Join("/etc", "openstack", "clouds-public.yaml")} + } + for _, publicPath := range options.publicLocations { + f, err := os.Open(publicPath) + if err != nil { + continue + } + defer f.Close() + options.cloudsPublicyamlReader = f + break + } + if options.cloudsPublicyamlReader == nil { + return cloud, fmt.Errorf("clouds file not found. Search locations were: %v", options.publicLocations) + } + } + + var publicClouds PublicClouds + if err := yaml.NewDecoder(options.cloudsPublicyamlReader).Decode(&publicClouds); err != nil { + return cloud, err + } + + pCloud, ok := publicClouds.Clouds[mergeWith] + if !ok { + return cloud, nil + } + + var err error + cloud, err = mergeClouds(cloud, pCloud) + if err != nil { + return cloud, fmt.Errorf("unable to merge information from clouds-public.yaml: %w", err) + } + + return cloud, nil + +} + // computeAvailability is a helper method to determine the endpoint type // requested by the user. func computeAvailability(endpointType string) gophercloud.Availability { diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/options.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/options.go index 863aaba..8c75683 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/options.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/options.go @@ -7,10 +7,12 @@ import ( ) type cloudOpts struct { - cloudName string - locations []string - cloudsyamlReader io.Reader - secureyamlReader io.Reader + cloudName string + locations []string + publicLocations []string + cloudsyamlReader io.Reader + secureyamlReader io.Reader + cloudsPublicyamlReader io.Reader applicationCredentialID string applicationCredentialName string @@ -53,6 +55,15 @@ func WithLocations(locations ...string) ParseOption { } } +// WithPublicLocations is a functional option that sets the search location for +// the clouds-public.yaml. Each location is a file path pointing to a possible +// `clouds-public.yaml` +func WithPublicLocations(locations ...string) ParseOption { + return func(co *cloudOpts) { + co.publicLocations = locations + } +} + // WithCloudsYAML is a functional option that lets you pass a clouds.yaml file // as an io.Reader interface. When this option is passed, FromCloudsYaml will // not attempt to fetch any file from the file system. To add a secure.yaml, @@ -72,6 +83,14 @@ func WithSecureYAML(secure io.Reader) ParseOption { } } +// WithCloudsPublicYAML is a functional option that lets you pass +// clouds-public.yaml file as an io.Reader interface +func WithCloudsPublicYAML(public io.Reader) ParseOption { + return func(co *cloudOpts) { + co.cloudsPublicyamlReader = public + } +} + func WithApplicationCredentialID(applicationCredentialID string) ParseOption { return func(co *cloudOpts) { co.applicationCredentialID = applicationCredentialID diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/types.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/types.go index 93fc093..946266a 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/types.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/config/clouds/types.go @@ -9,6 +9,10 @@ type Clouds struct { Clouds map[string]Cloud `yaml:"clouds" json:"clouds"` } +type PublicClouds struct { + Clouds map[string]Cloud `yaml:"public-clouds" json:"public-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"` diff --git a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/endpoints/results.go b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/endpoints/results.go index 7dbb1c9..a74c3fc 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/endpoints/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/openstack/identity/v3/endpoints/results.go @@ -56,8 +56,12 @@ type Endpoint struct { Name string `json:"name"` // Region is the region the Endpoint is located in. + // Deprecated in v3.2 in favor of region_id. Region string `json:"region"` + // RegionID is the ID of the region the Endpoint is located in. + RegionID string `json:"region_id"` + // ServiceID is the ID of the service the Endpoint refers to. ServiceID string `json:"service_id"` 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 index af79975..82a2bf5 100644 --- 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 @@ -17,5 +17,27 @@ Example to List Providers for _, p := range allProviders { fmt.Printf("%+v\n", p) } + +Example to List Provider Flavor Capabilities + + capabilities, err := providers.ListFlavorCapabilities(context.TODO(), lbClient, "amphora", nil).Extract() + if err != nil { + panic(err) + } + + for _, capability := range capabilities { + fmt.Printf("%+v\n", capability) + } + +Example to List Provider Availability Zone Capabilities + + capabilities, err := providers.ListAvailabilityZoneCapabilities(context.TODO(), lbClient, "amphora", nil).Extract() + if err != nil { + panic(err) + } + + for _, capability := range capabilities { + fmt.Printf("%+v\n", capability) + } */ 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 index 8b8e07c..7eb609d 100644 --- 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 @@ -1,6 +1,8 @@ package providers import ( + "context" + "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/pagination" ) @@ -42,3 +44,79 @@ func List(c *gophercloud.ServiceClient, opts ListOptsBuilder) pagination.Pager { return ProviderPage{pagination.LinkedPageBase{PageResult: r}} }) } + +// ListFlavorCapabilitiesOptsBuilder allows extensions to add additional +// parameters to the ListFlavorCapabilities request. +type ListFlavorCapabilitiesOptsBuilder interface { + ToFlavorCapabilitiesListQuery() (string, error) +} + +// ListFlavorCapabilitiesOpts allows the result of a ListFlavorCapabilities +// request to be filtered. +type ListFlavorCapabilitiesOpts struct { + // Fields is a list of attributes to return for each capability. + Fields []string `q:"fields"` +} + +// ToFlavorCapabilitiesListQuery formats a ListFlavorCapabilitiesOpts into a +// query string. +func (opts ListFlavorCapabilitiesOpts) ToFlavorCapabilitiesListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListFlavorCapabilities lists the flavor capabilities of a provider. +// This operation requires Octavia API version 2.6 or later. +func ListFlavorCapabilities(ctx context.Context, c *gophercloud.ServiceClient, provider string, opts ListFlavorCapabilitiesOptsBuilder) (r ListFlavorCapabilitiesResult) { + url := flavorCapabilitiesURL(c, provider) + if opts != nil { + query, err := opts.ToFlavorCapabilitiesListQuery() + if err != nil { + r.Err = err + return + } + url += query + } + + resp, err := c.Get(ctx, url, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} + +// ListAvailabilityZoneCapabilitiesOptsBuilder allows extensions to add +// additional parameters to the ListAvailabilityZoneCapabilities request. +type ListAvailabilityZoneCapabilitiesOptsBuilder interface { + ToAvailabilityZoneCapabilitiesListQuery() (string, error) +} + +// ListAvailabilityZoneCapabilitiesOpts allows the result of a +// ListAvailabilityZoneCapabilities request to be filtered. +type ListAvailabilityZoneCapabilitiesOpts struct { + // Fields is a list of attributes to return for each capability. + Fields []string `q:"fields"` +} + +// ToAvailabilityZoneCapabilitiesListQuery formats a +// ListAvailabilityZoneCapabilitiesOpts into a query string. +func (opts ListAvailabilityZoneCapabilitiesOpts) ToAvailabilityZoneCapabilitiesListQuery() (string, error) { + q, err := gophercloud.BuildQueryString(opts) + return q.String(), err +} + +// ListAvailabilityZoneCapabilities lists the availability zone capabilities +// of a provider. This operation requires Octavia API version 2.14 or later. +func ListAvailabilityZoneCapabilities(ctx context.Context, c *gophercloud.ServiceClient, provider string, opts ListAvailabilityZoneCapabilitiesOptsBuilder) (r ListAvailabilityZoneCapabilitiesResult) { + url := availabilityZoneCapabilitiesURL(c, provider) + if opts != nil { + query, err := opts.ToAvailabilityZoneCapabilitiesListQuery() + if err != nil { + r.Err = err + return + } + url += query + } + + resp, err := c.Get(ctx, url, &r.Body, nil) + _, r.Header, r.Err = gophercloud.ParseResponse(resp, err) + return +} 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 index ec374fa..3cc1caf 100644 --- 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 @@ -73,3 +73,45 @@ func (r commonResult) Extract() (*Provider, error) { type GetResult struct { commonResult } + +// Capability represents a provider-specific metadata key that can be used in +// a flavor profile or an availability zone profile. +type Capability struct { + // Name is the name of the capability. + Name string `json:"name"` + + // Description is a human-readable description of the capability. + Description string `json:"description"` +} + +// ListFlavorCapabilitiesResult represents the result of a request to list a +// provider's flavor capabilities. +type ListFlavorCapabilitiesResult struct { + gophercloud.Result +} + +// Extract interprets a ListFlavorCapabilitiesResult as a slice of +// capabilities. +func (r ListFlavorCapabilitiesResult) Extract() ([]Capability, error) { + var s struct { + Capabilities []Capability `json:"flavor_capabilities"` + } + err := r.ExtractInto(&s) + return s.Capabilities, err +} + +// ListAvailabilityZoneCapabilitiesResult represents the result of a request +// to list a provider's availability zone capabilities. +type ListAvailabilityZoneCapabilitiesResult struct { + gophercloud.Result +} + +// Extract interprets a ListAvailabilityZoneCapabilitiesResult as a slice of +// capabilities. +func (r ListAvailabilityZoneCapabilitiesResult) Extract() ([]Capability, error) { + var s struct { + Capabilities []Capability `json:"availability_zone_capabilities"` + } + err := r.ExtractInto(&s) + return s.Capabilities, err +} 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 index bd74a84..0aeac3c 100644 --- 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 @@ -3,10 +3,20 @@ package providers import "github.com/gophercloud/gophercloud/v2" const ( - rootPath = "lbaas" - resourcePath = "providers" + rootPath = "lbaas" + resourcePath = "providers" + flavorCapabilitiesPath = "flavor_capabilities" + availabilityZoneCapabilitiesPath = "availability_zone_capabilities" ) func rootURL(c *gophercloud.ServiceClient) string { return c.ServiceURL(rootPath, resourcePath) } + +func flavorCapabilitiesURL(c *gophercloud.ServiceClient, provider string) string { + return c.ServiceURL(rootPath, resourcePath, provider, flavorCapabilitiesPath) +} + +func availabilityZoneCapabilitiesURL(c *gophercloud.ServiceClient, provider string) string { + return c.ServiceURL(rootPath, resourcePath, provider, availabilityZoneCapabilitiesPath) +} diff --git a/vendor/github.com/gophercloud/gophercloud/v2/pagination/http.go b/vendor/github.com/gophercloud/gophercloud/v2/pagination/http.go index cf188b8..2f52d1e 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/pagination/http.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/pagination/http.go @@ -1,6 +1,7 @@ package pagination import ( + "bytes" "context" "encoding/json" "io" @@ -29,7 +30,9 @@ func PageResultFrom(resp *http.Response) (PageResult, error) { } if strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") { - err = json.Unmarshal(rawBody, &parsedBody) + dec := json.NewDecoder(bytes.NewReader(rawBody)) + dec.UseNumber() + err = dec.Decode(&parsedBody) if err != nil { return PageResult{}, err } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/params.go b/vendor/github.com/gophercloud/gophercloud/v2/params.go index ddb61b3..aa78967 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/params.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/params.go @@ -88,9 +88,12 @@ func BuildRequestBody(opts any, parent string) (map[string]any, error) { // if the field has a required tag that's set to "true" if requiredTag := f.Tag.Get("required"); requiredTag == "true" { //fmt.Printf("Checking required field [%s]:\n\tv: %+v\n\tisZero:%v\n", f.Name, v.Interface(), zero) - // if the field's value is zero, return a missing-argument error - if zero { - // if the field has a 'required' tag, it can't have a zero-value + // if the field's value is zero, return a missing-argument error. + // Numeric zero values are exempt: zero is legitimate input for + // several OpenStack APIs (for example, Keystone accepts 0 as a + // registered limit), so range validation is left to the server. + if zero && !isNumeric(v) { + // Required non-numeric fields cannot have a zero value. err := ErrMissingInput{} err.Argument = f.Name return nil, err @@ -302,6 +305,18 @@ func isUnderlyingStructZero(v reflect.Value) bool { var t time.Time +// isNumeric reports whether v holds a numeric value (integer, unsigned +// integer or floating point number). +func isNumeric(v reflect.Value) bool { + switch v.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + return true + } + return false +} + func isZero(v reflect.Value) bool { //fmt.Printf("\n\nchecking isZero for value: %+v\n", v) switch v.Kind() { @@ -353,8 +368,9 @@ converted into query parameters based on a "q" tag. For example: will be converted into "?x_bar=AAA&lorem_ipsum=BBB". -The struct's fields may be strings, integers, slices, or boolean values. Fields -left at their type's zero value will be omitted from the query. +The struct's fields may be strings, integers, slices, or boolean values. Optional +fields left at their type's zero value will be omitted from the query. Required +int fields are included even when zero; other required zero values return an error. Slice are handled in one of two ways: @@ -386,8 +402,12 @@ func BuildQueryString(opts any) (*url.URL, error) { if qTag != "" { tags := strings.Split(qTag, ",") - // if the field is set, add it to the slice of query pieces - if !isZero(v) { + required := f.Tag.Get("required") == "true" + + // if the field is set, add it to the slice of query pieces. + // Required int fields are included even when zero. Only exempt + // numeric kinds supported by the serialization switch below. + if !isZero(v) || (required && v.Kind() == reflect.Int) { loop: switch v.Kind() { case reflect.Pointer: @@ -427,8 +447,9 @@ func BuildQueryString(opts any) (*url.URL, error) { } } } else { - // if the field has a 'required' tag, it can't have a zero-value - if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + // Required fields cannot have a zero value unless they are + // numeric kinds supported by the serialization switch above. + if required { return &url.URL{}, fmt.Errorf("required query parameter [%s] not set", f.Name) } } @@ -466,8 +487,9 @@ will be converted into: "lorem_ipsum": "BBB", } -Untagged fields and fields left at their zero values are skipped. Integers, -booleans and string values are supported. +Untagged fields and optional fields left at their zero values are skipped. +Integers, booleans and string values are supported. Required int and int64 fields +are included even when zero; other required zero values return an error. */ func BuildHeaders(opts any) (map[string]string, error) { optsValue := reflect.ValueOf(opts) @@ -491,8 +513,12 @@ func BuildHeaders(opts any) (map[string]string, error) { if hTag != "" { tags := strings.Split(hTag, ",") - // if the field is set, add it to the slice of query pieces - if !isZero(v) { + required := f.Tag.Get("required") == "true" + + // if the field is set, add it to the map. Required int and int64 + // fields are included even when zero. Only exempt numeric kinds + // supported by the serialization switch below. + if !isZero(v) || (required && (v.Kind() == reflect.Int || v.Kind() == reflect.Int64)) { if v.Kind() == reflect.Pointer { v = v.Elem() } @@ -507,8 +533,9 @@ func BuildHeaders(opts any) (map[string]string, error) { optsMap[tags[0]] = strconv.FormatBool(v.Bool()) } } else { - // if the field has a 'required' tag, it can't have a zero-value - if requiredTag := f.Tag.Get("required"); requiredTag == "true" { + // Required fields cannot have a zero value unless they are + // numeric kinds supported by the serialization switch above. + if required { return optsMap, fmt.Errorf("required header [%s] not set", f.Name) } } diff --git a/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go b/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go index 8406dae..5310de3 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/provider_client.go @@ -14,7 +14,7 @@ import ( // DefaultUserAgent is the default User-Agent string set in the request header. const ( - DefaultUserAgent = "gophercloud/v2.14.0" + DefaultUserAgent = "gophercloud/v2.15.0" DefaultMaxBackoffRetries = 60 ) @@ -546,7 +546,13 @@ func (client *ProviderClient) doRequest(ctx context.Context, method, url string, _, err = io.Copy(io.Discard, resp.Body) return resp, err } - if err := json.NewDecoder(resp.Body).Decode(options.JSONResponse); err != nil { + // UseNumber keeps integers that do not fit in float64 (for example + // signed 64-bit maximum BIOS bounds) exact when JSONResponse is any. + // Extract() round-trips Body through json.Marshal; float64 rounding + // makes that second unmarshal fail for int fields. + dec := json.NewDecoder(resp.Body) + dec.UseNumber() + if err := dec.Decode(options.JSONResponse); err != nil { if client.RetryFunc != nil { var e error state.retries = state.retries + 1 diff --git a/vendor/github.com/gophercloud/gophercloud/v2/results.go b/vendor/github.com/gophercloud/gophercloud/v2/results.go index 675bba1..c1472b3 100644 --- a/vendor/github.com/gophercloud/gophercloud/v2/results.go +++ b/vendor/github.com/gophercloud/gophercloud/v2/results.go @@ -72,10 +72,29 @@ func (r Result) extractIntoPtr(to any, label string) error { return r.ExtractInto(&to) } + // Decode into map[string]any with UseNumber so integers that do not fit + // in float64 survive the extra marshal/unmarshal below. ExtractInto() is + // not used here because it would decode numbers into any as float64. var m map[string]any - err := r.ExtractInto(&m) - if err != nil { - return err + if reader, ok := r.Body.(io.Reader); ok { + if readCloser, ok := reader.(io.Closer); ok { + defer readCloser.Close() + } + dec := json.NewDecoder(reader) + dec.UseNumber() + if err := dec.Decode(&m); err != nil { + return err + } + } else { + b, err := json.Marshal(r.Body) + if err != nil { + return err + } + dec := json.NewDecoder(bytes.NewReader(b)) + dec.UseNumber() + if err := dec.Decode(&m); err != nil { + return err + } } // Check if the expected label exists in the response diff --git a/vendor/modules.txt b/vendor/modules.txt index 33a5cc1..66d0de5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# github.com/gophercloud/gophercloud/v2 v2.14.0 +# github.com/gophercloud/gophercloud/v2 v2.15.0 ## explicit; go 1.25.0 github.com/gophercloud/gophercloud/v2 github.com/gophercloud/gophercloud/v2/openstack