From 32a2b4c27f7e0768ac8f1b5c99f5f3217bcf8019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Du=C5=A1an=20Jakub?= Date: Tue, 17 Jun 2025 10:59:24 +0200 Subject: [PATCH] Feature: Support for OpenTelecom Cloud CCE clusters --- README.md | 1 + cmd/switcher/switcher.go | 9 + docs/stores/otc/otc.md | 69 ++ go.mod | 3 +- go.sum | 2 + pkg/store/kubeconfig_store_otc.go | 264 +++++ pkg/store/plugins/example/go.mod | 12 +- pkg/store/plugins/example/go.sum | 6 + .../kubeconfigstore/v1/kubeconfig_store.pb.go | 5 +- .../v1/kubeconfig_store_grpc.pb.go | 1 + pkg/store/types.go | 7 + types/config.go | 8 +- .../gophertelekomcloud/.editorconfig | 32 + .../gophertelekomcloud/.gitignore | 4 + .../gophertelekomcloud/.golangci.yaml | 9 + .../gophertelekomcloud/.zuul.yaml | 19 + .../gophertelekomcloud/CHANGELOG.md | 0 .../gophertelekomcloud/CODEOWNERS | 2 + .../gophertelekomcloud/FAQ.md | 148 +++ .../gophertelekomcloud/LICENSE | 204 ++++ .../gophertelekomcloud/MIGRATING.md | 32 + .../gophertelekomcloud/Makefile | 28 + .../gophertelekomcloud/README.md | 110 ++ .../gophertelekomcloud/STYLEGUIDE.md | 76 ++ .../gophertelekomcloud/auth_aksk_options.go | 47 + .../auth_option_provider.go | 6 + .../gophertelekomcloud/auth_options.go | 452 ++++++++ .../gophertelekomcloud/doc.go | 93 ++ .../gophertelekomcloud/endpoint_search.go | 76 ++ .../gophertelekomcloud/errors.go | 428 ++++++++ .../gophertelekomcloud/internal/build/doc.go | 4 + .../gophertelekomcloud/internal/build/errs.go | 7 + .../internal/build/headers.go | 106 ++ .../internal/build/query_string.go | 127 +++ .../internal/build/request_body.go | 92 ++ .../gophertelekomcloud/internal/build/tags.go | 98 ++ .../internal/extract/doc.go | 3 + .../internal/extract/json.go | 164 +++ .../internal/multierr/multierr.go | 57 + .../gophertelekomcloud/openstack/auth_env.go | 50 + .../openstack/cce/v3/clusters/doc.go | 55 + .../openstack/cce/v3/clusters/requests.go | 283 +++++ .../openstack/cce/v3/clusters/results.go | 318 ++++++ .../openstack/cce/v3/clusters/urls.go | 25 + .../gophertelekomcloud/openstack/client.go | 970 ++++++++++++++++++ .../gophertelekomcloud/openstack/doc.go | 14 + .../openstack/endpoint_location.go | 57 + .../openstack/endpoint_util.go | 21 + .../gophertelekomcloud/openstack/errors.go | 59 ++ .../openstack/identity/v3/catalog/requests.go | 14 + .../openstack/identity/v3/catalog/results.go | 27 + .../openstack/identity/v3/catalog/urls.go | 7 + .../openstack/identity/v3/domains/doc.go | 59 ++ .../openstack/identity/v3/domains/requests.go | 129 +++ .../openstack/identity/v3/domains/results.go | 97 ++ .../openstack/identity/v3/domains/urls.go | 23 + .../openstack/identity/v3/projects/doc.go | 58 ++ .../identity/v3/projects/requests.go | 132 +++ .../openstack/identity/v3/projects/results.go | 103 ++ .../openstack/identity/v3/projects/urls.go | 23 + .../openstack/identity/v3/tokens/doc.go | 108 ++ .../openstack/identity/v3/tokens/requests.go | 229 +++++ .../openstack/identity/v3/tokens/results.go | 179 ++++ .../openstack/identity/v3/tokens/urls.go | 7 + .../gophertelekomcloud/openstack/loader.go | 697 +++++++++++++ .../openstack/utils/base_endpoint.go | 28 + .../openstack/utils/choose_version.go | 111 ++ .../openstack/utils/utils.go | 102 ++ .../gophertelekomcloud/pagination/http.go | 106 ++ .../gophertelekomcloud/pagination/info.go | 54 + .../gophertelekomcloud/pagination/linked.go | 110 ++ .../gophertelekomcloud/pagination/marker.go | 54 + .../gophertelekomcloud/pagination/offset.go | 60 ++ .../gophertelekomcloud/pagination/pager.go | 440 ++++++++ .../gophertelekomcloud/pagination/pkg.go | 4 + .../gophertelekomcloud/pagination/single.go | 56 + .../gophertelekomcloud/params.go | 523 ++++++++++ .../gophertelekomcloud/provider_client.go | 464 +++++++++ .../gophertelekomcloud/results.go | 381 +++++++ .../gophertelekomcloud/results_job.go | 100 ++ .../gophertelekomcloud/service_client.go | 183 ++++ .../service_client_extension.go | 23 + .../gophertelekomcloud/signer_helper.go | 519 ++++++++++ .../gophertelekomcloud/util.go | 100 ++ vendor/modules.txt | 14 + 85 files changed, 9977 insertions(+), 10 deletions(-) create mode 100644 docs/stores/otc/otc.md create mode 100644 pkg/store/kubeconfig_store_otc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/.editorconfig create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/.gitignore create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/.golangci.yaml create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/.zuul.yaml create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/CHANGELOG.md create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/CODEOWNERS create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/FAQ.md create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/LICENSE create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/MIGRATING.md create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/Makefile create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/README.md create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/STYLEGUIDE.md create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_aksk_options.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_option_provider.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_options.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/endpoint_search.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/errors.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/errs.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/headers.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/query_string.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/request_body.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/tags.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/json.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/multierr/multierr.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/auth_env.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/requests.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/results.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/urls.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/client.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_location.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_util.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/errors.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/requests.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/results.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/urls.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/requests.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/results.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/urls.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/requests.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/results.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/urls.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/doc.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/requests.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/results.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/urls.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/loader.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/base_endpoint.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/choose_version.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/utils.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/http.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/info.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/linked.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/marker.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/offset.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/pager.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/pkg.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/single.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/params.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/provider_client.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/results.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/results_job.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client_extension.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/signer_helper.go create mode 100644 vendor/github.com/opentelekomcloud/gophertelekomcloud/util.go diff --git a/README.md b/README.md index 2d478405e..fe0964a38 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Designed as a [drop-in replacement](#difference-to-kubectx) for [kubectx](https: - Scaleway (documentation tbd) - [Akamai / Linode](docs/stores/akamai/akamai.md) - [Cluster API (capi)](docs/stores/capi/capi.md) + - [OpenTelekom Cloud (OTC)](docs/stores/otc/otc.md) - Your favorite Cloud Provider or Managed Kubernetes Platform is not supported yet? Looking for contributions! - **Change the namespace** - **Change to any context and namespace from the history** diff --git a/cmd/switcher/switcher.go b/cmd/switcher/switcher.go index 056f831a6..dd4ce8009 100644 --- a/cmd/switcher/switcher.go +++ b/cmd/switcher/switcher.go @@ -325,6 +325,15 @@ func initialize() ([]storetypes.KubeconfigStore, *types.Config, error) { return nil, nil, err } s = capiStore + case types.StoreKindOTC: + capiStore, err := store.NewOTCStore(kubeconfigStoreFromConfig, stateDirectory) + if err != nil { + if kubeconfigStoreFromConfig.Required != nil && !*kubeconfigStoreFromConfig.Required { + continue + } + return nil, nil, err + } + s = capiStore case types.StoreKindPlugin: pluginStore, err := store.NewPluginStore(kubeconfigStoreFromConfig) if err != nil { diff --git a/docs/stores/otc/otc.md b/docs/stores/otc/otc.md new file mode 100644 index 000000000..29efb1373 --- /dev/null +++ b/docs/stores/otc/otc.md @@ -0,0 +1,69 @@ +# OpenTelekom Cloud CCE store + +Kubeswitch can discover CCE clusters from OTC. + +Kubeswitch relies on the [Openstask configuration files](https://docs.openstack.org/python-openstackclient/latest/configuration/index.html), with additions as defined by the underlying [SDK](https://github.com/opentelekomcloud/gophertelekomcloud) + +## Clouds configuration + +First create a file `~/.config/openstack/clouds.yaml` + +``` +cat ~/.config/openstack/clouds.yaml + +clouds: + my-cloud-with-permanent-aksk: + auth: + auth_url: https://iam.eu-de.otc.t-systems.com:443/v3 + project_name: eu-de_xxx # also known as tenant name + ak: YOUR_AK + sk: YOUR_SK + interface: public + identity_api_version: "3" + auth_type: aksk + my-cloud-with-temporary-aksk: + auth: + auth_url: https://iam.eu-de.otc.t-systems.com/v3 + project_name: eu-de_yyy # also known as tenant name + ak: YOUR_TEMPORARY_AK + sk: YOUR_TEMPORARY_SK + security_token: YOUR_TEMPORARY_STS + interface: public + identity_api_version: "3" + auth_type: aksk + +``` + +Permanent and temporary AK/SK as well as temporary STS can be generated in OTC web console after you log in. + +Other authentication methods supported by the format and SDK (password, federated, token, assume role, ...) may also work, but were not tested. + + +## Kubectx configuration + +``` +cat ~/.kube/switch-config.yaml + +kind: SwitchConfig +version: "v1alpha1" +kubeconfigStores: + - kind: otc + id: otc1 + config: + cloud: my-cloud-with-permanent-aksk + + - kind: otc + id: otc2 + config: + cloud: my-cloud-with-temporary-aksk + selectContext: externalTLSVerify +``` + +Retrieved kubeconfig for each CCE cluster contains 3 contexts (same ones as when it is downloaded from the web console): +- internal +- external +- externalTLSVerify + +By default, all three are available for selection by `kubeswitch`. + +The optinal parameter `selectContext` allows for selecting only one of these contexts for each cluster. \ No newline at end of file diff --git a/go.mod b/go.mod index 3bc890795..5d4b3d18d 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.19.1 github.com/onsi/gomega v1.34.0 + github.com/opentelekomcloud/gophertelekomcloud v0.9.3 github.com/pkg/errors v0.9.1 github.com/rancher/norman v0.0.0-20240205154641-a6a6cf569608 github.com/rancher/rancher/pkg/client v0.0.0-20240416202124-a6da228939da @@ -48,6 +49,7 @@ require ( require ( github.com/digitalocean/doctl v1.105.0 github.com/digitalocean/godo v1.113.0 + github.com/exoscale/egoscale/v3 v3.1.9 github.com/hashicorp/go-plugin v1.6.2 github.com/linode/linodego v1.42.0 github.com/ovh/go-ovh v1.4.3 @@ -97,7 +99,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/exoscale/egoscale/v3 v3.1.9 // indirect github.com/fatih/color v1.17.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect diff --git a/go.sum b/go.sum index c759968e9..2435ddb66 100644 --- a/go.sum +++ b/go.sum @@ -382,6 +382,8 @@ github.com/opencontainers/image-spec v1.1.0-rc3 h1:fzg1mXZFj8YdPeNkRXMg+zb88BFV0 github.com/opencontainers/image-spec v1.1.0-rc3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= +github.com/opentelekomcloud/gophertelekomcloud v0.9.3 h1:zdttgRAWc4uHgJ3PX5hP8ulhT1VYBh2JeRsItNPp8dg= +github.com/opentelekomcloud/gophertelekomcloud v0.9.3/go.mod h1:M1F6OfSRZRzAmAFKQqSLClX952at5hx5rHe4UTEykgg= github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0= github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= diff --git a/pkg/store/kubeconfig_store_otc.go b/pkg/store/kubeconfig_store_otc.go new file mode 100644 index 000000000..ba2c8efd9 --- /dev/null +++ b/pkg/store/kubeconfig_store_otc.go @@ -0,0 +1,264 @@ +// Copyright 2021 The Kubeswitch 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 store + +import ( + "context" + "fmt" + "github.com/disiqueira/gotree" + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters" + "strings" + "time" + + "github.com/opentelekomcloud/gophertelekomcloud/openstack" + + storetypes "github.com/danielfoehrkn/kubeswitch/pkg/store/types" + "github.com/danielfoehrkn/kubeswitch/types" + "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" +) + +func NewOTCStore(store types.KubeconfigStore, _ string) (*OTCStore, error) { + otcStoreConfig := &types.StoreConfigOTC{} + if store.Config != nil { + buf, err := yaml.Marshal(store.Config) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(buf, otcStoreConfig) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal OTC config: %w", err) + } + } + + return &OTCStore{ + KubeconfigStore: store, + Config: otcStoreConfig, + }, nil +} + +func (s *OTCStore) InitializeOTCStore() error { + _, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var names []string + if *s.Config.Cloud != "" { + names = []string{*s.Config.Cloud} + } + env := openstack.NewEnv("OTC_") + cloud, err := env.Cloud(names...) + if err != nil { + return fmt.Errorf("error getting cloud: %w", err) + } + + // Cannot do this - there is a bug in gophertelekomcloud that STS key is not loaded from config + // client, err := openstack.AuthenticatedClientFromCloud(cloud) + // if err != nil { + // return fmt.Errorf("error getting client: %w", err) + // } + + opts, err := openstack.AuthOptionsFromInfo(&cloud.AuthInfo, cloud.AuthType) + if err != nil { + return fmt.Errorf("failed to convert AuthInfo to AuthOptsBuilder with Env vars: %s", err) + } + + // fix the issue from upstream + if aksk, ok := opts.(golangsdk.AKSKAuthOptions); ok { + if aksk.SecurityToken == "" && cloud.AuthInfo.SecurityToken != "" { + aksk.SecurityToken = cloud.AuthInfo.SecurityToken + opts = aksk + } + } + + // finally get the client + client, err := openstack.AuthenticatedClient(opts) + if err != nil { + return fmt.Errorf("failed to authenticate client: %s", err) + } + + cceClient, err := openstack.NewCCE(client, golangsdk.EndpointOpts{}) + if err != nil { + return fmt.Errorf("Error getting cce: %+v\n", err) + } + + s.Client = cceClient + return nil +} + +func (s *OTCStore) IsInitialized() bool { + return s.Client != nil && s.Config != nil +} + +func (s *OTCStore) GetID() string { + id := "default" + + if s.KubeconfigStore.ID != nil { + id = *s.KubeconfigStore.ID + } + + return fmt.Sprintf("%s.%s", types.StoreKindOTC, id) +} + +func (s *OTCStore) GetKind() types.StoreKind { + return types.StoreKindOTC +} + +func (s *OTCStore) GetStoreConfig() types.KubeconfigStore { + return s.KubeconfigStore +} + +func (s *OTCStore) GetLogger() *logrus.Entry { + if s.Logger == nil { + s.Logger = logrus.WithField("store", s.GetID()) + } + return s.Logger +} + +func (s *OTCStore) GetContextPrefix(path string) string { + if s.GetStoreConfig().ShowPrefix != nil && !*s.GetStoreConfig().ShowPrefix { + return "" + } + + return strings.ReplaceAll(path, "--", "-") +} + +func (s *OTCStore) VerifyKubeconfigPaths() error { + // NOOP + return nil +} + +func (s *OTCStore) StartSearch(channel chan storetypes.SearchResult) { + _, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := s.InitializeOTCStore(); err != nil { + err := fmt.Errorf("failed to initialize store. This is most likely a problem with your provided OTC credentials: %v", err) + channel <- storetypes.SearchResult{ + Error: err, + } + return + } + + refinedClusters, err := clusters.List(s.Client, clusters.ListOpts{}) + if err != nil { + channel <- storetypes.SearchResult{ + Error: fmt.Errorf("Error getting clusters: %+v\n", err), + } + } + + for _, cluster := range refinedClusters { + kubeconfigPath := fmt.Sprintf("otc_%s--%s", *s.Config.Cloud, cluster.Metadata.Name) + channel <- storetypes.SearchResult{ + KubeconfigPath: kubeconfigPath, + Error: nil, + Tags: map[string]string{ + "id": cluster.Metadata.Id, + "name": cluster.Metadata.Name, + "version": cluster.Spec.Version, + "type": cluster.Spec.Type, + "flavor": cluster.Spec.Flavor, + "status": cluster.Status.Phase, + }, + } + } + s.GetLogger().Debugf("Search done for OTC") +} + +func (s *OTCStore) GetKubeconfigForPath(_ string, tags map[string]string) ([]byte, error) { + _, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if !s.IsInitialized() { + if err := s.InitializeOTCStore(); err != nil { + return nil, fmt.Errorf("failed to initialize OTC store: %w", err) + } + } + + expiryOpts := clusters.ExpirationOpts{ + Duration: -1, + } + + kubeconfig, err := clusters.GetCertWithExpiration(s.Client, tags["id"], expiryOpts).ExtractMap() + if err != nil { + return nil, fmt.Errorf("unable to retrieve cluster kubeconfig: %w", err) + } + + kubeconfig = s.convertKubeconfig(tags["name"], kubeconfig) + + configYaml, err := yaml.Marshal(kubeconfig) + if err != nil { + return nil, fmt.Errorf("unable to marshal cluster kubeconfig: %w", err) + } + return configYaml, nil +} + +func (s *OTCStore) convertKubeconfig(name string, kubeconfig map[string]interface{}) map[string]interface{} { + if s.Config.SelectContext == nil { + return kubeconfig + } + + contexts, ok := kubeconfig["contexts"].([]interface{}) + if !ok { + s.GetLogger().Warnf("contexts in kubeconfig is not an array, skipping context selection") + return kubeconfig + } + + var selectedContext = make(map[string]interface{}) + for _, ctx := range contexts { + ctxMap, ok := ctx.(map[string]interface{}) + if !ok { + s.GetLogger().Warnf("context %v is not a map, skipping", ctx) + continue + } + + if ctxMap["name"] == *s.Config.SelectContext { + selectedContext = ctxMap + break + } + } + + if selectedContext == nil { + s.GetLogger().Warnf("selected context %q not found in kubeconfig", *s.Config.SelectContext) + return kubeconfig + } + + selectedContext["name"] = name + kubeconfig["contexts"] = []interface{}{selectedContext} + return kubeconfig +} + +func (s *OTCStore) GetSearchPreview(_ string, tags map[string]string) (string, error) { + _, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if !s.IsInitialized() { + if err := s.InitializeOTCStore(); err != nil { + return "", fmt.Errorf("failed to initialize OTC store: %w", err) + } + } + + asciTree := gotree.New(tags["name"]) + + asciTree.Add(fmt.Sprintf("ID: %s", tags["id"])) + asciTree.Add(fmt.Sprintf("Cloud: %s", *s.Config.Cloud)) + asciTree.Add(fmt.Sprintf("Version: %s", tags["version"])) + asciTree.Add(fmt.Sprintf("Type: %s", tags["type"])) + asciTree.Add(fmt.Sprintf("Flavor: %s", tags["flavor"])) + asciTree.Add(fmt.Sprintf("Status: %s", tags["status"])) + + return asciTree.Print(), nil +} diff --git a/pkg/store/plugins/example/go.mod b/pkg/store/plugins/example/go.mod index bcf4ca272..11f34860d 100644 --- a/pkg/store/plugins/example/go.mod +++ b/pkg/store/plugins/example/go.mod @@ -16,12 +16,12 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/oklog/run v1.0.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.68.0 // indirect - google.golang.org/protobuf v1.34.2 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d // indirect + google.golang.org/grpc v1.69.2 // indirect + google.golang.org/protobuf v1.36.1 // indirect k8s.io/apimachinery v0.31.2 // indirect ) diff --git a/pkg/store/plugins/example/go.sum b/pkg/store/plugins/example/go.sum index fad30000e..6d9cadd0b 100644 --- a/pkg/store/plugins/example/go.sum +++ b/pkg/store/plugins/example/go.sum @@ -42,6 +42,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -52,14 +53,19 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250102185135-69823020774d/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= +google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store.pb.go b/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store.pb.go index 7d84df947..dc015e9a1 100644 --- a/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store.pb.go +++ b/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store.pb.go @@ -7,10 +7,11 @@ package kubeconfigstorev1 import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store_grpc.pb.go b/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store_grpc.pb.go index dc1a4c0c5..599d800fc 100644 --- a/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store_grpc.pb.go +++ b/pkg/store/plugins/kubeconfigstore/v1/kubeconfig_store_grpc.pb.go @@ -8,6 +8,7 @@ package kubeconfigstorev1 import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/pkg/store/types.go b/pkg/store/types.go index 14e66d307..5424978b0 100644 --- a/pkg/store/types.go +++ b/pkg/store/types.go @@ -15,6 +15,7 @@ package store import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" "sync" "github.com/danielfoehrkn/kubeswitch/pkg/store/doks" @@ -179,3 +180,9 @@ type PluginStore struct { Config *types.StoreConfigPlugin Client plugins.Store } +type OTCStore struct { + Logger *logrus.Entry + KubeconfigStore types.KubeconfigStore + Client *golangsdk.ServiceClient + Config *types.StoreConfigOTC +} diff --git a/types/config.go b/types/config.go index 810b59e08..f65e50d02 100644 --- a/types/config.go +++ b/types/config.go @@ -24,7 +24,7 @@ import ( type StoreKind string // ValidStoreKinds contains all valid store kinds -var ValidStoreKinds = sets.NewString(string(StoreKindVault), string(StoreKindFilesystem), string(StoreKindGardener), string(StoreKindGKE), string(StoreKindAzure), string(StoreKindEKS), string(StoreKindExoscale), string(StoreKindRancher), string(StoreKindOVH), string(StoreKindScaleway), string(StoreKindDigitalOcean), string(StoreKindAkamai), string(StoreKindCapi), string(StoreKindPlugin)) +var ValidStoreKinds = sets.NewString(string(StoreKindVault), string(StoreKindFilesystem), string(StoreKindGardener), string(StoreKindGKE), string(StoreKindAzure), string(StoreKindEKS), string(StoreKindExoscale), string(StoreKindRancher), string(StoreKindOVH), string(StoreKindScaleway), string(StoreKindDigitalOcean), string(StoreKindAkamai), string(StoreKindCapi), string(StoreKindPlugin), string(StoreKindOTC)) // ValidConfigVersions contains all valid config versions var ValidConfigVersions = sets.NewString("v1alpha1") @@ -58,6 +58,7 @@ const ( StoreKindCapi StoreKind = "capi" // StoreKindPlugin is an identifier for the Plugin store StoreKindPlugin StoreKind = "plugin" + StoreKindOTC StoreKind = "otc" ) type Config struct { @@ -207,6 +208,11 @@ type StoreConfigEKS struct { Profile string `yaml:"profile"` } +type StoreConfigOTC struct { + Cloud *string `yaml:"cloud"` + SelectContext *string `yaml:"selectContext"` +} + // GCPAuthenticationType // Required permission to list GKE clusters: container.clusters.list // Requires to have the container.clusters.get permission. The least-privileged IAM role that provides this permission is container.clusterViewer. diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/.editorconfig b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.editorconfig new file mode 100644 index 000000000..3dc802c93 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.editorconfig @@ -0,0 +1,32 @@ +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.go] +indent_style = tab +indent_size = 4 +ij_go_add_leading_space_to_comments = true +ij_go_add_parentheses_for_single_import = false +ij_go_call_parameters_new_line_after_left_paren = true +ij_go_call_parameters_right_paren_on_new_line = true +ij_go_call_parameters_wrap = off +ij_go_fill_paragraph_width = 80 +ij_go_group_current_project_imports = true +ij_go_group_stdlib_imports = true +ij_go_import_sorting = gofmt +ij_go_keep_indents_on_empty_lines = false +ij_go_move_all_imports_in_one_declaration = true +ij_go_move_all_stdlib_imports_in_one_group = true +ij_go_remove_redundant_import_aliases = false +ij_go_use_back_quotes_for_imports = false +ij_go_wrap_comp_lit = off +ij_go_wrap_comp_lit_newline_after_lbrace = true +ij_go_wrap_comp_lit_newline_before_rbrace = true +ij_go_wrap_func_params = off +ij_go_wrap_func_params_newline_after_lparen = true +ij_go_wrap_func_params_newline_before_rparen = true +ij_go_wrap_func_result = off +ij_go_wrap_func_result_newline_after_lparen = true +ij_go_wrap_func_result_newline_before_rparen = true diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/.gitignore b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.gitignore new file mode 100644 index 000000000..41eb9e601 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.gitignore @@ -0,0 +1,4 @@ +**/*.swp +.idea +.DS_Store +.env diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/.golangci.yaml b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.golangci.yaml new file mode 100644 index 000000000..1234edce7 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.golangci.yaml @@ -0,0 +1,9 @@ +linters-settings: + staticcheck: + checks: + - all + # Exclude some staticcheck messages + - '-SA1008' # "content-length" is not canonical, avoid OBS headers warnings + - '-SA1019' # deprecations, used to avoid Extract... deprecation warnings +run: + timeout: 5m diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/.zuul.yaml b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.zuul.yaml new file mode 100644 index 000000000..3e6164d60 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/.zuul.yaml @@ -0,0 +1,19 @@ +--- +- project: + merge-mode: squash-merge + vars: + functest_project_name: "eu-de_zuul_go" + check: + jobs: + - otc-golangci-lint + - golang-make-vet + - golang-make-test + check-post: + jobs: + - golang-make-functional + gate: + jobs: + - otc-golangci-lint + - golang-make-vet + - golang-make-test + - golang-make-functional diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/CHANGELOG.md b/vendor/github.com/opentelekomcloud/gophertelekomcloud/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/CODEOWNERS b/vendor/github.com/opentelekomcloud/gophertelekomcloud/CODEOWNERS new file mode 100644 index 000000000..9e43318fc --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/CODEOWNERS @@ -0,0 +1,2 @@ +* @anton-sidelnikov +* @artem-lifshits diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/FAQ.md b/vendor/github.com/opentelekomcloud/gophertelekomcloud/FAQ.md new file mode 100644 index 000000000..c0337dfbe --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/FAQ.md @@ -0,0 +1,148 @@ +# Tips + +## Implementing default logging and re-authentication attempts + +You can implement custom logging and/or limit re-auth attempts by creating a custom HTTP client +like the following and setting it as the provider client's HTTP Client (via the +`golangsdk.ProviderClient.HTTPClient` field): + +```go +//... + +// LogRoundTripper satisfies the http.RoundTripper interface and is used to +// customize the default Gophercloud RoundTripper to allow for logging. +type LogRoundTripper struct { + rt http.RoundTripper + numReauthAttempts int +} + +// newHTTPClient return a custom HTTP client that allows for logging relevant +// information before and after the HTTP request. +func newHTTPClient() http.Client { + return http.Client{ + Transport: &LogRoundTripper{ + rt: http.DefaultTransport, + }, + } +} + +// RoundTrip performs a round-trip HTTP request and logs relevant information about it. +func (lrt *LogRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { + glog.Infof("Request URL: %s\n", request.URL) + + response, err := lrt.rt.RoundTrip(request) + if response == nil { + return nil, err + } + + if response.StatusCode == http.StatusUnauthorized { + if lrt.numReauthAttempts == 3 { + return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.") + } + lrt.numReauthAttempts++ + } + + glog.Debugf("Response Status: %s\n", response.Status) + + return response, nil +} + +endpoint := "https://127.0.0.1/auth" +pc := openstack.NewClient(endpoint) +pc.HTTPClient = newHTTPClient() + +//... +``` + + +## Implementing custom objects + +OpenStack request/response objects may differ among variable names or types. + +### Custom request objects + +To pass custom options to a request, implement the desired `OptsBuilder` interface. For +example, to pass in + +```go +type MyCreateServerOpts struct { + Name string + Size int +} +``` + +to `servers.Create`, simply implement the `servers.CreateOptsBuilder` interface: + +```go +func (o MyCreateServeropts) ToServerCreateMap() (map[string]interface{}, error) { + return map[string]interface{}{ + "name": o.Name, + "size": o.Size, + }, nil +} +``` + +create an instance of your custom options object, and pass it to `servers.Create`: + +```go +// ... +myOpts := MyCreateServerOpts{ + Name: "s1", + Size: "100", +} +server, err := servers.Create(computeClient, myOpts).Extract() +// ... +``` + +### Custom response objects + +Some OpenStack services have extensions. Extensions that are supported in Gophercloud can be +combined to create a custom object: + +```go +// ... +type MyVolume struct { + volumes.Volume + tenantattr.VolumeExt +} + +var v struct { + MyVolume `json:"volume"` +} + +err := volumes.Get(client, volID).ExtractInto(&v) +// ... +``` + +## Overriding default `UnmarshalJSON` method + +For some response objects, a field may be a custom type or may be allowed to take on +different types. In these cases, overriding the default `UnmarshalJSON` method may be +necessary. To do this, declare the JSON `struct` field tag as "-" and create an `UnmarshalJSON` +method on the type: + +```go +// ... +type MyVolume struct { + ID string `json: "id"` + TimeCreated time.Time `json: "-"` +} + +func (r *MyVolume) UnmarshalJSON(b []byte) error { + type tmp MyVolume + var s struct { + tmp + TimeCreated golangsdk.JSONRFC3339MilliNoZ `json:"created_at"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Volume(s.tmp) + + r.TimeCreated = time.Time(s.CreatedAt) + + return err +} +// ... +``` diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/LICENSE b/vendor/github.com/opentelekomcloud/gophertelekomcloud/LICENSE new file mode 100644 index 000000000..52e0eb594 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/LICENSE @@ -0,0 +1,204 @@ + + 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 2012-2013 Rackspace, Inc. + Copyright 2018-2020 Huawei Technologies Co., Ltd. + Copyright 2020 T-Systems International GmbH + + 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/opentelekomcloud/gophertelekomcloud/MIGRATING.md b/vendor/github.com/opentelekomcloud/gophertelekomcloud/MIGRATING.md new file mode 100644 index 000000000..a52444c9b --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/MIGRATING.md @@ -0,0 +1,32 @@ +# Compute + +## Floating IPs + +* `github.com/opentelekomcloud/gophertelekomcloud/openstack/compute/v2/extensions/floatingip` is now `github.com/opentelekomcloud/gophertelekomcloud/openstack/compute/v2/extensions/floatingips` +* `floatingips.Associate` and `floatingips.Disassociate` have been removed. +* `floatingips.DisassociateOpts` is now required to disassociate a Floating IP. + +## Security Groups + +* `secgroups.AddServerToGroup` is now `secgroups.AddServer`. +* `secgroups.RemoveServerFromGroup` is now `secgroups.RemoveServer`. + +## Servers + +* `servers.Reboot` now requires a `servers.RebootOpts` struct: + + ```golang + rebootOpts := &servers.RebootOpts{ + Type: servers.SoftReboot, + } + res := servers.Reboot(client, server.ID, rebootOpts) + ``` + +# Identity + +## V3 + +### Tokens + +* `Token.ExpiresAt` is now of type `golangsdk.JSONRFC3339Milli` instead of + `time.Time` diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/Makefile b/vendor/github.com/opentelekomcloud/gophertelekomcloud/Makefile new file mode 100644 index 000000000..430d8b318 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/Makefile @@ -0,0 +1,28 @@ +export GO111MODULE=on +export PATH:=/usr/local/go/bin:$(PATH) +exec_path := /usr/local/bin/ +exec_name := gophertelekomcloud + + +default: test +test: test-unit +acceptance: test-acc + +fmt: + @echo Running go fmt + @go fmt + +lint: + @echo Running go lint + @golangci-lint run --timeout=300s + +vet: + @echo "go vet ." + @go vet ./... + +test-unit: + @go test ./openstack/... -parallel 4 -v + +test-acc: + @echo "Starting acceptance tests..." + @go test ./acceptance/... -race -covermode=atomic -coverprofile=coverage.txt -timeout 20m -v diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/README.md b/vendor/github.com/opentelekomcloud/gophertelekomcloud/README.md new file mode 100644 index 000000000..c1196cb8f --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/README.md @@ -0,0 +1,110 @@ +# GopherTelekomCloud: a OpenTelekomCloud SDK for Golang + +[![Go Report Card](https://goreportcard.com/badge/github.com/opentelekomcloud/gophertelekomcloud?branch=devel)](https://goreportcard.com/report/github.com/opentelekomcloud/gophertelekomcloud) +[![Zuul Gated](https://zuul-ci.org/gated.svg)](https://zuul.eco.tsi-dev.otc-service.com/t/eco/buildsets?project=opentelekomcloud%2Fgophertelekomcloud&pipeline=gate) +[![LICENSE](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://github.com/opentelekomcloud/gophertelekomcloud/blob/master/LICENSE) + +GopherTelekomCloud is a OpenTelekomCloud clouds Go SDK. GopherTelekomCloud is based +on [Gophercloud](https://github.com/gophercloud/gophercloud) +which is an OpenStack Go SDK and has a great design. GopherTelekomCloud has added and removed some features to support +OpenTelekomCloud. + +## Useful links + +* [Reference documentation](http://godoc.org/github.com/opentelekomcloud/gophertelekomcloud) +* [Effective Go](https://golang.org/doc/effective_go.html) + +## How to install + +Installation with modern Go and `go mod` is really simple: + +Just run `go mod download` to install all dependencies. + +## Getting started + +### Credentials + +Because you'll be hitting an API, you will need to retrieve your OpenTelekomCloud credentials and store them using +standard Openstack approaches: +either [`clouds.yaml`](https://docs.openstack.org/python-openstackclient/latest/configuration/index.html) +file (recommended) or environment variables. + +You will need to retrieve the following: + +* domain name +* username +* password +* project name/id (for most of the services) +* a valid IAM identity URL + +### Authentication + +Once you have access to your credentials, you can begin plugging them into Golangsdk. The next step is authentication, +and this is handled by a base +"Provider" struct. To get one, you can either pass in your credentials explicitly, or tell Golangsdk to use environment +variables: + +#### Option 1: Pass in the values yourself + +```go +opts := golangsdk.AuthOptions{ +IdentityEndpoint: "https://openstack.example.com:5000/v2.0", +Username: "{username}", +Password: "{password}", +} +client, err := openstack.AuthenticatedClient(opts) +``` + +#### Option 2: Use a utility function to retrieve cloud configuration from env variables and configuration files + +```go +env := openstack.NewEnv("OS_") // use OS_ prefixed env variables +client, err := env.AuthenticatedClient() +``` + +The `ProviderClient` is the top-level client that all of your OpenTelekomCloud services derive from. The provider +contains all of the authentication details that allow your Go code to access the API - such as the base URL and token +ID. + +### Provision a rds instance + +Once we have a base Provider, we inject it as a dependency into each OpenTelekomCloud service. In order to work with the +rds API, we need a rds service client; which can be created like so: + +```go +client, err := openstack.NewRdsServiceV1(provider, golangsdk.EndpointOpts{ + Region: utils.GetRegion(ao), +}) +``` + +We then use this `client` for any rds API operation we want. In our case, we want to provision a rds instance - so we +invoke the `Create` method and pass in the name and the flavor ID (database specification) we're interested in: + +```go +import "github.com/opentelekomcloud/gophertelekomcloud/openstack/rds/v1/instances" + +instance, err := instances.Create(client, instances.CreateOpts{ + Name: "My new rds instance!", + FlavorRef: "flavor_id", +}).Extract() +``` + +The above code sample creates a new rds instance with the parameters, and embodies the new resource in the `instance` +variable (a[`instances.Instance`](http://godoc.org/github.com/opentelekomcloud/gophertelekomcloud) struct). + +## Advanced Usage + +Have a look at the [FAQ](./FAQ.md) for some tips on customizing the way Golangsdk works. + +## Backwards-Compatibility Guarantees + +None. Vendor it and write tests covering the parts you use. + +## Contributing + +See the [contributing guide](./.github/CONTRIBUTING.md). + +## Help and feedback + +If you're struggling with something or have spotted a potential bug, feel free to submit an issue to +our [bug tracker](https://github.com/opentelekomcloud/gophertelekomcloud/issues). diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/STYLEGUIDE.md b/vendor/github.com/opentelekomcloud/gophertelekomcloud/STYLEGUIDE.md new file mode 100644 index 000000000..7c0e137ed --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/STYLEGUIDE.md @@ -0,0 +1,76 @@ + +## On Pull Requests + +- Please make sure to read our [contributing guide](/.github/CONTRIBUTING.md). + +- Before you start a PR there needs to be a Github issue and a discussion about it + on that issue with a core contributor, even if it's just a 'SGTM'. + +- A PR's description must reference the issue it closes with a `For ` (e.g. For #293). + +- A PR's description must contain link(s) to the line(s) in the OpenStack + source code (on Github) that prove(s) the PR code to be valid. Links to documentation + are not good enough. The link(s) should be to a non-`master` branch. For example, + a pull request implementing the creation of a Neutron v2 subnet might put the + following link in the description: + + https://github.com/openstack/neutron/blob/stable/mitaka/neutron/api/v2/attributes.py#L749 + + From that link, a reviewer (or user) can verify the fields in the request/response + objects in the PR. + +- A PR that is in-progress should have `[wip]` in front of the PR's title. When + ready for review, remove the `[wip]` and ping a core contributor with an `@`. + +- Forcing PRs to be small can have the effect of users submitting PRs in a hierarchical chain, with + one depending on the next. If a PR depends on another one, it should have a [Pending #PRNUM] + prefix in the PR title. In addition, it will be the PR submitter's responsibility to remove the + [Pending #PRNUM] tag once the PR has been updated with the merged, dependent PR. That will + let reviewers know it is ready to review. + +- A PR should be small. Even if you intend on implementing an entire + service, a PR should only be one route of that service + (e.g. create server or get server, but not both). + +- Unless explicitly asked, do not squash commits in the middle of a review; only + append. It makes it difficult for the reviewer to see what's changed from one + review to the next. + +## On Code + +- In re design: follow as closely as is reasonable the code already in the library. + Most operations (e.g. create, delete) admit the same design. + +- Unit tests and acceptance (integration) tests must be written to cover each PR. + Tests for operations with several options (e.g. list, create) should include all + the options in the tests. This will allow users to verify an operation on their + own infrastructure and see an example of usage. + +- If in doubt, ask in-line on the PR. + +### File Structure + +- The following should be used in most cases: + + - `requests.go`: contains all the functions that make HTTP requests and the + types associated with the HTTP request (parameters for URL, body, etc) + - `results.go`: contains all the response objects and their methods + - `urls.go`: contains the endpoints to which the requests are made + +### Naming + +- For methods on a type in `results.go`, the receiver should be named `r` and the + variable into which it will be unmarshalled `s`. + +- Functions in `requests.go`, with the exception of functions that return a + `pagination.Pager`, should be named returns of the name `r`. + +- Functions in `requests.go` that accept request bodies should accept as their + last parameter an `interface` named `OptsBuilder` (eg `CreateOptsBuilder`). + This `interface` should have at the least a method named `ToMap` + (eg `ToPortCreateMap`). + +- Functions in `requests.go` that accept query strings should accept as their + last parameter an `interface` named `OptsBuilder` (eg `ListOptsBuilder`). + This `interface` should have at the least a method named `ToQuery` + (eg `ToServerListQuery`). diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_aksk_options.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_aksk_options.go new file mode 100644 index 000000000..a12753e1e --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_aksk_options.go @@ -0,0 +1,47 @@ +package golangsdk + +// AKSKAuthOptions presents the required information for AK/SK auth +type AKSKAuthOptions 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. + // + // The IdentityEndpoint is typically referred to as the "auth_url" or + // "OS_AUTH_URL" in the information provided by the cloud operator. + IdentityEndpoint string `json:"-"` + + // user project id + ProjectId string + + ProjectName string + + // region + Region string + + // cloud service domain + Domain string + DomainID string + + // cloud service domain for BSS + BssDomain string + BssDomainID string + + AccessKey string // Access Key + SecretKey string // Secret key + SecurityToken string // Security token (part of temporary AK/SK) + + // AgencyName is the name of agency + AgencyName string + + // AgencyDomainName is the name of domain who created the agency + AgencyDomainName string + + // DelegatedProject is the name of delegated project + DelegatedProject string +} + +// GetIdentityEndpoint implements the method of AKSKAuthOptions +func (opts AKSKAuthOptions) GetIdentityEndpoint() string { + return opts.IdentityEndpoint +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_option_provider.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_option_provider.go new file mode 100644 index 000000000..755a3eb93 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_option_provider.go @@ -0,0 +1,6 @@ +package golangsdk + +// AuthOptionsProvider presents the base of an auth options implementation +type AuthOptionsProvider interface { + GetIdentityEndpoint() string +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_options.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_options.go new file mode 100644 index 000000000..b27ad3b3e --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/auth_options.go @@ -0,0 +1,452 @@ +package golangsdk + +import "github.com/opentelekomcloud/gophertelekomcloud/internal/build" + +/* +AuthOptions stores information needed to authenticate to an OpenStack Cloud. +You can populate one manually, or use a provider's AuthOptionsFromEnv() function +to read relevant information from the standard environment variables. Pass one +to a provider's AuthenticatedClient function to authenticate and obtain a +ProviderClient representing an active session on that provider. + +Its fields are the union of those recognized by each identity implementation and +provider. + +An example of manually providing authentication information: + + opts := golangsdk.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +An example of using AuthOptionsFromEnv(), where the environment variables can +be read from a file, such as a standard openrc file: + + opts, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(opts) +*/ +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. + // + // The IdentityEndpoint is typically referred to as the "auth_url" or + // "OS_AUTH_URL" in the information provided by the cloud operator. + 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:"-"` + + Password string `json:"password,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"` + + // The TenantID and TenantName fields are optional for the Identity V2 API. + // The same fields are known as project_id and project_name in the Identity + // V3 API, but are collected as TenantID and TenantName here in both cases. + // 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. + // If DomainID or DomainName are provided, they will also apply to TenantName. + // It is not currently possible to authenticate with Username and a Domain + // and scope to a Project in a different Domain by using TenantName. To + // accomplish that, the ProjectID will need to be provided as the TenantID + // option. + TenantID string `json:"tenantId,omitempty"` + TenantName string `json:"tenantName,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. + // + // NOTE: The reauth function will try to re-authenticate endlessly if left + // unchecked. The way to limit the number of attempts is to provide a custom + // HTTP client to the provider client and provide a transport that implements + // the RoundTripper interface and stores the number of failed retries. For an + // example of this, see here: + // https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311 + AllowReauth bool `json:"-"` + + // TokenID allows users to authenticate (possibly as another user) with an + // authentication token ID. + TokenID string `json:"-"` + + // AgencyName is the name of agency + AgencyName string `json:"-"` + + // AgencyDomainName is the name of domain who created the agency + AgencyDomainName string `json:"-"` + + // DelegatedProject is the name of delegated project + DelegatedProject string `json:"-"` + + // Passcode is a Virtual MFA device verification code, which can be obtained on the MFA app. + Passcode string `json:"-"` +} + +// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder +// interface in the v2 tokens package +func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) { + // Populate the request map. + authMap := make(map[string]interface{}) + + if opts.Username != "" { + if opts.Password != "" { + authMap["passwordCredentials"] = map[string]interface{}{ + "username": opts.Username, + "password": opts.Password, + } + } else { + return nil, ErrMissingInput{Argument: "Password"} + } + } else if opts.TokenID != "" { + authMap["token"] = map[string]interface{}{ + "id": opts.TokenID, + } + } else { + return nil, ErrMissingInput{Argument: "Username"} + } + + if opts.TenantID != "" { + authMap["tenantId"] = opts.TenantID + } + if opts.TenantName != "" { + authMap["tenantName"] = opts.TenantName + } + + return map[string]interface{}{"auth": authMap}, nil +} + +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + type domainReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + } + + type userReq struct { + ID *string `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + Password string `json:"password"` + Domain *domainReq `json:"domain,omitempty"` + } + + type passwordReq struct { + User userReq `json:"user"` + } + + type tokenReq struct { + ID string `json:"id"` + } + + type totpUserReq struct { + ID string `json:"id"` + Passcode string `json:"passcode"` + } + + type totpReq struct { + User totpUserReq `json:"user"` + } + + type identityReq struct { + Methods []string `json:"methods"` + Password *passwordReq `json:"password,omitempty"` + Token *tokenReq `json:"token,omitempty"` + TOTP *totpReq `json:"totp,omitempty"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + } + + type request struct { + Auth authReq `json:"auth"` + } + + // Populate the request structure based on the provided arguments. Create and return an error + // if insufficient or incompatible information is present. + var req request + + if opts.Password == "" { + if opts.TokenID != "" { + // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication + // parameters. + if opts.Username != "" { + return nil, ErrUsernameWithToken{} + } + if opts.UserID != "" { + return nil, ErrUserIDWithToken{} + } + + // Configure the request for Token authentication. + req.Auth.Identity.Methods = []string{"token"} + req.Auth.Identity.Token = &tokenReq{ + ID: opts.TokenID, + } + } else { + // If no password or token ID are available, authentication can't continue. + return nil, ErrMissingPassword{} + } + } else { + // Password authentication. + req.Auth.Identity.Methods = []string{"password"} + + // At least one of Username and UserID must be specified. + if opts.Username == "" && opts.UserID == "" { + return nil, ErrUsernameOrUserID{} + } + + if opts.Username != "" { + // If Username is provided, UserID may not be provided. + if opts.UserID != "" { + return nil, ErrUsernameOrUserID{} + } + + // Either DomainID or DomainName must also be specified. + if opts.DomainID == "" && opts.DomainName == "" { + return nil, ErrDomainIDOrDomainName{} + } + + if opts.DomainID != "" { + if opts.DomainName != "" { + return nil, ErrDomainIDOrDomainName{} + } + + // Configure the request for Username and Password authentication with a DomainID. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{ID: &opts.DomainID}, + }, + } + } + + if opts.DomainName != "" { + // Configure the request for Username and Password authentication with a DomainName. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ + Name: &opts.Username, + Password: opts.Password, + Domain: &domainReq{Name: &opts.DomainName}, + }, + } + } + } + + if opts.UserID != "" { + // Configure the request for UserID and Password authentication. + req.Auth.Identity.Password = &passwordReq{ + User: userReq{ID: &opts.UserID, Password: opts.Password}, + } + } + if opts.Passcode != "" { + if opts.UserID == "" { + return nil, ErrUserIDNotFound{} + } + req.Auth.Identity.TOTP = &totpReq{ + User: totpUserReq{ + ID: opts.UserID, + Passcode: opts.Passcode, + }, + } + req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "totp") + } + } + + b, err := build.RequestBodyMap(req, "") + if err != nil { + return nil, err + } + + if len(scope) != 0 { + b["auth"].(map[string]interface{})["scope"] = scope + } + + return b, nil +} + +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + var scope scopeInfo + + if opts.TenantID != "" { + scope.ProjectID = opts.TenantID + } else { + if opts.TenantName != "" { + scope.ProjectName = opts.TenantName + scope.DomainID = opts.DomainID + scope.DomainName = opts.DomainName + } else { + // support scoping to domain + scope.DomainID = opts.DomainID + scope.DomainName = opts.DomainName + } + } + return scope.BuildTokenV3ScopeMap() +} + +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +func (opts *AuthOptions) AuthTokenID() string { + return opts.TokenID +} + +func (opts *AuthOptions) AuthHeaderDomainID() string { + return opts.DomainID +} + +// Implements the method of AuthOptionsProvider +func (opts AuthOptions) GetIdentityEndpoint() string { + return opts.IdentityEndpoint +} + +type scopeInfo struct { + ProjectID string + ProjectName string + DomainID string + DomainName string +} + +func (scope *scopeInfo) BuildTokenV3ScopeMap() (map[string]interface{}, error) { + if scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if scope.DomainID == "" && scope.DomainName == "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + if scope.ProjectID != "" { + return nil, ErrScopeProjectIDOrProjectName{} + } + + if scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &scope.ProjectName, + "domain": map[string]interface{}{"id": &scope.DomainID}, + }, + }, nil + } + + if scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &scope.ProjectName, + "domain": map[string]interface{}{"name": &scope.DomainName}, + }, + }, nil + } + } else if scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if scope.DomainID != "" { + return nil, ErrScopeProjectIDAlone{} + } + if scope.DomainName != "" { + return nil, ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &scope.ProjectID, + }, + }, nil + } else if scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if scope.DomainName != "" { + return nil, ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &scope.DomainID, + }, + }, nil + } else if scope.DomainName != "" { + // DomainName + return map[string]interface{}{ + "domain": map[string]interface{}{ + "name": &scope.DomainName, + }, + }, nil + } + + return nil, nil +} + +type AgencyAuthOptions struct { + TokenID string + DomainID string + AgencyName string + AgencyDomainName string + DelegatedProject string +} + +func (opts *AgencyAuthOptions) CanReauth() bool { + return false +} + +func (opts *AgencyAuthOptions) AuthTokenID() string { + return opts.TokenID +} + +func (opts *AgencyAuthOptions) AuthHeaderDomainID() string { + return opts.DomainID +} + +func (opts *AgencyAuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + scope := scopeInfo{ + ProjectName: opts.DelegatedProject, + DomainName: opts.AgencyDomainName, + } + + return scope.BuildTokenV3ScopeMap() +} + +func (opts *AgencyAuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + type assumeRoleReq struct { + DomainName string `json:"domain_name"` + AgencyName string `json:"xrole_name"` + } + + type identityReq struct { + Methods []string `json:"methods"` + AssumeRole assumeRoleReq `json:"assume_role"` + } + + type authReq struct { + Identity identityReq `json:"identity"` + } + + var req authReq + req.Identity.Methods = []string{"assume_role"} + req.Identity.AssumeRole = assumeRoleReq{ + DomainName: opts.AgencyDomainName, + AgencyName: opts.AgencyName, + } + r, err := build.RequestBodyMap(req, "auth") + if err != nil { + return r, err + } + + if len(scope) != 0 { + r["auth"].(map[string]interface{})["scope"] = scope + } + return r, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/doc.go new file mode 100644 index 000000000..fc429a15a --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/doc.go @@ -0,0 +1,93 @@ +/* +Package golangsdk provides a multi-vendor interface to OpenStack-compatible +clouds. The library has a three-level hierarchy: providers, services, and +resources. + +Authenticating with Providers + +Provider structs represent the cloud providers that offer and manage a +collection of services. You will generally want to create one Provider +client per OpenStack cloud. + +Use your OpenStack credentials to create a Provider client. The +IdentityEndpoint is typically refered to as "auth_url" or "OS_AUTH_URL" in +information provided by the cloud operator. Additionally, the cloud may refer to +TenantID or TenantName as project_id and project_name. Credentials are +specified like so: + + opts := golangsdk.AuthOptions{ + IdentityEndpoint: "https://openstack.example.com:5000/v2.0", + Username: "{username}", + Password: "{password}", + TenantID: "{tenant_id}", + } + + provider, err := openstack.AuthenticatedClient(opts) + +You may also use the openstack.AuthOptionsFromEnv() helper function. This +function reads in standard environment variables frequently found in an +OpenStack `openrc` file. Again note that Gophercloud currently uses "tenant" +instead of "project". + + opts, err := openstack.AuthOptionsFromEnv() + provider, err := openstack.AuthenticatedClient(opts) + +Service Clients + +Service structs are specific to a provider and handle all of the logic and +operations for a particular OpenStack service. Examples of services include: +Compute, Object Storage, Block Storage. In order to define one, you need to +pass in the parent provider, like so: + + opts := golangsdk.EndpointOpts{Region: "RegionOne"} + + client := openstack.NewComputeV2(provider, opts) + +Resources + +Resource structs are the domain models that services make use of in order +to work with and represent the state of API resources: + + server, err := servers.Get(client, "{serverId}").Extract() + +Intermediate Result structs are returned for API operations, which allow +generic access to the HTTP headers, response body, and any errors associated +with the network transaction. To turn a result into a usable resource struct, +you must call the Extract method which is chained to the response, or an +Extract function from an applicable extension: + + result := servers.Get(client, "{serverId}") + + // Attempt to extract the disk configuration from the OS-DCF disk config + // extension: + config, err := diskconfig.ExtractGet(result) + +All requests that enumerate a collection return a Pager struct that is used to +iterate through the results one page at a time. Use the EachPage method on that +Pager to handle each successive Page in a closure, then use the appropriate +extraction method from that request's package to interpret that Page as a slice +of results: + + err := servers.List(client, nil).EachPage(func (page pagination.Page) (bool, error) { + s, err := servers.ExtractServers(page) + if err != nil { + return false, err + } + + // Handle the []servers.Server slice. + + // Return "false" or an error to prematurely stop fetching new pages. + return true, nil + }) + +If you want to obtain the entire collection of pages without doing any +intermediary processing on each page, you can use the AllPages method: + + allPages, err := servers.List(client, nil).AllPages() + allServers, err := servers.ExtractServers(allPages) + +This top-level package contains utility functions and data types that are used +throughout the provider and service packages. Of particular note for end users +are the AuthOptions and EndpointOpts structs. +*/ +package golangsdk diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/endpoint_search.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/endpoint_search.go new file mode 100644 index 000000000..dd86a9cc2 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/endpoint_search.go @@ -0,0 +1,76 @@ +package golangsdk + +// Availability indicates to whom a specific service endpoint is accessible: +// the internet at large, internal networks only, or only to administrators. +// Different identity services use different terminology for these. Identity v2 +// lists them as different kinds of URLs within the service catalog ("adminURL", +// "internalURL", and "publicURL"), while v3 lists them as "Interfaces" in an +// endpoint's response. +type Availability string + +const ( + // AvailabilityAdmin indicates that an endpoint is only available to + // administrators. + AvailabilityAdmin Availability = "admin" + + // AvailabilityPublic indicates that an endpoint is available to everyone on + // the internet. + AvailabilityPublic Availability = "public" + + // AvailabilityInternal indicates that an endpoint is only available within + // the cluster's internal network. + AvailabilityInternal Availability = "internal" +) + +// EndpointOpts specifies search criteria used by queries against an +// OpenStack service catalog. The options must contain enough information to +// unambiguously identify one, and only one, endpoint within the catalog. +// +// Usually, these are passed to service client factory functions in a provider +// package, like "openstack.NewComputeV2()". +type EndpointOpts struct { + // Type [required] is the service type for the client (e.g., "compute", + // "object-store"). Generally, this will be supplied by the service client + // function, but a user-given value will be honored if provided. + Type string + + // Name [optional] is the service name for the client (e.g., "nova") as it + // appears in the service catalog. Services can have the same Type but a + // different Name, which is why both Type and Name are sometimes needed. + Name string + + // Region [required] is the geographic region in which the endpoint resides, + // generally specifying which datacenter should house your resources. + // Required only for services that span multiple regions. + Region string + + // Availability [optional] is the visibility of the endpoint to be returned. + // Valid types include the constants AvailabilityPublic, AvailabilityInternal, + // or AvailabilityAdmin from this package. + // + // Availability is not required, and defaults to AvailabilityPublic. Not all + // providers or services offer all Availability options. + Availability Availability +} + +/* +EndpointLocator is an internal function to be used by provider implementations. + +It provides an implementation that locates a single endpoint from a service +catalog for a specific ProviderClient based on user-provided EndpointOpts. The +provider then uses it to discover related ServiceClients. +*/ +type EndpointLocator func(EndpointOpts) (string, error) + +// ApplyDefaults is an internal method to be used by provider implementations. +// +// It sets EndpointOpts fields if not already set, including a default type. +// Currently, EndpointOpts.Availability defaults to the public endpoint. +func (eo *EndpointOpts) ApplyDefaults(t string) { + if eo.Type == "" { + eo.Type = t + } + if eo.Availability == "" { + eo.Availability = AvailabilityPublic + } +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/errors.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/errors.go new file mode 100644 index 000000000..ee4772c4d --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/errors.go @@ -0,0 +1,428 @@ +package golangsdk + +import "fmt" + +// BaseError is an error type that all other error types embed. +type BaseError struct { + DefaultErrString string + Info string +} + +func (e BaseError) Error() string { + e.DefaultErrString = "An error occurred while executing a Gophercloud request." + return e.choseErrString() +} + +func (e BaseError) choseErrString() string { + if e.Info != "" { + return e.Info + } + return e.DefaultErrString +} + +// ErrMissingInput is the error when input is required in a particular +// situation but not provided by the user +type ErrMissingInput struct { + BaseError + Argument string +} + +func (e ErrMissingInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Missing input for argument [%s]", e.Argument) + return e.choseErrString() +} + +// ErrInvalidInput is an error type used for most non-HTTP Gophercloud errors. +type ErrInvalidInput struct { + ErrMissingInput + Value interface{} +} + +func (e ErrInvalidInput) Error() string { + e.DefaultErrString = fmt.Sprintf("Invalid input provided for argument [%s]: [%+v]", e.Argument, e.Value) + return e.choseErrString() +} + +// ErrUnexpectedResponseCode is returned by the Request method when a response code other than +// those listed in OkCodes is encountered. +type ErrUnexpectedResponseCode struct { + BaseError + URL string + Method string + Expected []int + Actual int + Body []byte +} + +func (e ErrUnexpectedResponseCode) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Expected HTTP response code %v when accessing [%s %s], but got %d instead\n%s", + e.Expected, e.Method, e.URL, e.Actual, e.Body, + ) + return e.choseErrString() +} + +// ErrDefault400 is the default error type returned on a 400 HTTP response code. +type ErrDefault400 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault401 is the default error type returned on a 401 HTTP response code. +type ErrDefault401 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault403 is the default error type returned on a 403 HTTP response code. +type ErrDefault403 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault404 is the default error type returned on a 404 HTTP response code. +type ErrDefault404 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault405 is the default error type returned on a 405 HTTP response code. +type ErrDefault405 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault408 is the default error type returned on a 408 HTTP response code. +type ErrDefault408 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault409 is the default error type returned on a 409 HTTP response code. +type ErrDefault409 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault429 is the default error type returned on a 429 HTTP response code. +type ErrDefault429 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault500 is the default error type returned on a 500 HTTP response code. +type ErrDefault500 struct { + ErrUnexpectedResponseCode +} + +// ErrDefault503 is the default error type returned on a 503 HTTP response code. +type ErrDefault503 struct { + ErrUnexpectedResponseCode +} + +func (e ErrDefault400) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Bad request with: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault401) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Authentication Failed, error message: %s", e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault403) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Action Forbidden, error message: %s", e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault404) Error() string { + e.DefaultErrString = fmt.Sprintf( + "Resource not found: [%s %s], error message: %s", + e.Method, e.URL, e.Body, + ) + return e.choseErrString() +} +func (e ErrDefault405) Error() string { + return "Method not allowed" +} +func (e ErrDefault408) Error() string { + return "The server timed out waiting for the request" +} +func (e ErrDefault429) Error() string { + return "Too many requests have been sent in a given amount of time. Pause" + + " requests, wait up to one minute, and try again." +} +func (e ErrDefault500) Error() string { + return "Internal Server Error" +} +func (e ErrDefault503) Error() string { + return "The service is currently unable to handle the request due to a temporary" + + " overloading or maintenance. This is a temporary condition. Try again later." +} + +// Err400er is the interface resource error types implement to override the error message +// from a 400 error. +type Err400er interface { + Error400(ErrUnexpectedResponseCode) error +} + +// Err401er is the interface resource error types implement to override the error message +// from a 401 error. +type Err401er interface { + Error401(ErrUnexpectedResponseCode) error +} + +// Err403er is the interface resource error types implement to override the error message +// from a 403 error. +type Err403er interface { + Error403(ErrUnexpectedResponseCode) error +} + +// Err404er is the interface resource error types implement to override the error message +// from a 404 error. +type Err404er interface { + Error404(ErrUnexpectedResponseCode) error +} + +// Err405er is the interface resource error types implement to override the error message +// from a 405 error. +type Err405er interface { + Error405(ErrUnexpectedResponseCode) error +} + +// Err408er is the interface resource error types implement to override the error message +// from a 408 error. +type Err408er interface { + Error408(ErrUnexpectedResponseCode) error +} + +// Err409er is the interface resource error types implement to override the error message +// from a 409 error. +type Err409er interface { + Error409(ErrUnexpectedResponseCode) error +} + +// Err429er is the interface resource error types implement to override the error message +// from a 429 error. +type Err429er interface { + Error429(ErrUnexpectedResponseCode) error +} + +// Err500er is the interface resource error types implement to override the error message +// from a 500 error. +type Err500er interface { + Error500(ErrUnexpectedResponseCode) error +} + +// Err503er is the interface resource error types implement to override the error message +// from a 503 error. +type Err503er interface { + Error503(ErrUnexpectedResponseCode) error +} + +// ErrTimeOut is the error type returned when an operations times out. +type ErrTimeOut struct { + BaseError +} + +func (e ErrTimeOut) Error() string { + e.DefaultErrString = "A time out occurred" + return e.choseErrString() +} + +// ErrUnableToReauthenticate is the error type returned when reauthentication fails. +type ErrUnableToReauthenticate struct { + BaseError + ErrOriginal error +} + +func (e ErrUnableToReauthenticate) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to re-authenticate: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrErrorAfterReauthentication is the error type returned when reauthentication +// succeeds, but an error occurs afterword (usually an HTTP error). +type ErrErrorAfterReauthentication struct { + BaseError + ErrOriginal error +} + +func (e ErrErrorAfterReauthentication) Error() string { + e.DefaultErrString = fmt.Sprintf("Successfully re-authenticated, but got error executing request: %s", e.ErrOriginal) + return e.choseErrString() +} + +// ErrServiceNotFound is returned when no service in a service catalog matches +// the provided EndpointOpts. This is generally returned by provider service +// factory methods like "NewComputeV2()" and can mean that a service is not +// enabled for your account. +type ErrServiceNotFound struct { + BaseError +} + +func (e ErrServiceNotFound) Error() string { + e.DefaultErrString = "No suitable service could be found in the service catalog." + return e.choseErrString() +} + +// ErrEndpointNotFound is returned when no available endpoints match the +// provided EndpointOpts. This is also generally returned by provider service +// factory methods, and usually indicates that a region was specified +// incorrectly. +type ErrEndpointNotFound struct { + BaseError +} + +func (e ErrEndpointNotFound) Error() string { + e.DefaultErrString = "No suitable endpoint could be found in the service catalog." + return e.choseErrString() +} + +// ErrResourceNotFound is the error when trying to retrieve a resource's +// ID by name and the resource doesn't exist. +type ErrResourceNotFound struct { + BaseError + Name string + ResourceType string +} + +func (e ErrResourceNotFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Unable to find %s with name %s", e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrMultipleResourcesFound is the error when trying to retrieve a resource's +// ID by name and multiple resources have the user-provided name. +type ErrMultipleResourcesFound struct { + BaseError + Name string + Count int + ResourceType string +} + +func (e ErrMultipleResourcesFound) Error() string { + e.DefaultErrString = fmt.Sprintf("Found %d %ss matching %s", e.Count, e.ResourceType, e.Name) + return e.choseErrString() +} + +// ErrUnexpectedType is the error when an unexpected type is encountered +type ErrUnexpectedType struct { + BaseError + Expected string + Actual string +} + +func (e ErrUnexpectedType) Error() string { + e.DefaultErrString = fmt.Sprintf("Expected %s but got %s", e.Expected, e.Actual) + return e.choseErrString() +} + +func unacceptedAttributeErr(attribute string) string { + return fmt.Sprintf("The base Identity V3 API does not accept authentication by %s", attribute) +} + +func redundantWithTokenErr(attribute string) string { + return fmt.Sprintf("%s may not be provided when authenticating with a TokenID", attribute) +} + +// ErrAPIKeyProvided indicates that an APIKey was provided but can't be used. +type ErrAPIKeyProvided struct{ BaseError } + +func (e ErrAPIKeyProvided) Error() string { + return unacceptedAttributeErr("APIKey") +} + +// ErrTenantIDProvided indicates that a TenantID was provided but can't be used. +type ErrTenantIDProvided struct{ BaseError } + +func (e ErrTenantIDProvided) Error() string { + return unacceptedAttributeErr("TenantID") +} + +// ErrTenantNameProvided indicates that a TenantName was provided but can't be used. +type ErrTenantNameProvided struct{ BaseError } + +func (e ErrTenantNameProvided) Error() string { + return unacceptedAttributeErr("TenantName") +} + +// ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead. +type ErrUsernameWithToken struct{ BaseError } + +func (e ErrUsernameWithToken) Error() string { + return redundantWithTokenErr("Username") +} + +// ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead. +type ErrUserIDWithToken struct{ BaseError } + +func (e ErrUserIDWithToken) Error() string { + return redundantWithTokenErr("UserID") +} + +// ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead. +type ErrDomainIDWithToken struct{ BaseError } + +func (e ErrDomainIDWithToken) Error() string { + return redundantWithTokenErr("DomainID") +} + +// ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s +type ErrDomainNameWithToken struct{ BaseError } + +func (e ErrDomainNameWithToken) Error() string { + return redundantWithTokenErr("DomainName") +} + +// ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once. +type ErrUsernameOrUserID struct{ BaseError } + +func (e ErrUsernameOrUserID) Error() string { + return "Exactly one of Username and UserID must be provided for password authentication" +} + +// ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it. +// It may also indicate that both a DomainID and a DomainName were provided at once. +type ErrDomainIDOrDomainName struct{ BaseError } + +func (e ErrDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName to authenticate by Username" +} + +// ErrMissingPassword indicates that no password was provided and no token is available. +type ErrMissingPassword struct{ BaseError } + +func (e ErrMissingPassword) Error() string { + return "You must provide a password to authenticate" +} + +// ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present. +type ErrScopeDomainIDOrDomainName struct{ BaseError } + +func (e ErrScopeDomainIDOrDomainName) Error() string { + return "You must provide exactly one of DomainID or DomainName in a Scope with ProjectName" +} + +// ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope. +type ErrScopeProjectIDOrProjectName struct{ BaseError } + +func (e ErrScopeProjectIDOrProjectName) Error() string { + return "You must provide at most one of ProjectID or ProjectName in a Scope" +} + +// ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope. +type ErrScopeProjectIDAlone struct{ BaseError } + +func (e ErrScopeProjectIDAlone) Error() string { + return "ProjectID must be supplied alone in a Scope" +} + +// ErrScopeEmpty indicates that no credentials were provided in a Scope. +type ErrScopeEmpty struct{ BaseError } + +func (e ErrScopeEmpty) Error() string { + return "You must provide either a Project or Domain in a Scope" +} + +type ErrUserIDNotFound struct{ BaseError } + +func (e ErrUserIDNotFound) Error() string { + return "You must provide UserID for MFA" +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/doc.go new file mode 100644 index 000000000..0be47ac65 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/doc.go @@ -0,0 +1,4 @@ +/* +Package build contains internal methods for building request parts: query string, headers, body. +*/ +package build diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/errs.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/errs.go new file mode 100644 index 000000000..81880b421 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/errs.go @@ -0,0 +1,7 @@ +package build + +import "errors" + +// ErrNilOpts used to be returned in case opts passed are nil. +// This can be expected in some cases. +var ErrNilOpts = errors.New("nil options provided") diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/headers.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/headers.go new file mode 100644 index 000000000..146ab2423 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/headers.go @@ -0,0 +1,106 @@ +package build + +import ( + "fmt" + "reflect" + "strconv" + + "github.com/opentelekomcloud/gophertelekomcloud/internal/multierr" +) + +/* +Headers is an internal function to be used by request methods in +individual resource packages. + +It accepts an arbitrary tagged structure and produces a string map that's +suitable for use as the HTTP headers of an outgoing request. Field names are +mapped to header names based in "h" tags. + + type struct QueryStruct { + Bar string `h:"x_bar"` + Baz int `h:"lorem_ipsum"` + } + + instance := QueryStruct{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into: + + map[string]string{ + "x_bar": "AAA", + "lorem_ipsum": "BBB", + } + +Untagged fields and fields left at their zero values are skipped. Integers, +booleans and string values are supported. +*/ +func Headers(opts interface{}) (map[string]string, error) { + if opts == nil { + return nil, fmt.Errorf("error building headers: %w", ErrNilOpts) + } + + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + if optsValue.Kind() != reflect.Struct { + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("error building headers: options type is not a struct") + } + + mErr := multierr.MultiError{} + result := make(map[string]string) + + for i := 0; i < optsValue.NumField(); i++ { + value := optsValue.Field(i) + field := optsType.Field(i) + + headerName := field.Tag.Get("h") + if headerName == "" { + continue + } + + if value.IsZero() { + // We duplicate the check from ValidateTags to avoid double reflect package usage + // TODO: investigate performance difference when using ValidateTags + if structFieldRequired(field) { + mErr = append(mErr, fmt.Errorf("required header [%s] not set", field.Name)) + } + continue + } + + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + + var headerValue string + + // if the field is set, add it to the slice of query pieces + switch value.Kind() { + case reflect.String: + headerValue = value.String() + case reflect.Int, reflect.Int32, reflect.Int64: + headerValue = strconv.FormatInt(value.Int(), 10) + case reflect.Bool: + headerValue = strconv.FormatBool(value.Bool()) + default: + mErr = append(mErr, fmt.Errorf("value of unsupported type %s", value.Type())) + } + + result[headerName] = headerValue + } + + if err := mErr.ErrorOrNil(); err != nil { + return nil, fmt.Errorf("error building headers: %w", err) + } + + return result, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/query_string.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/query_string.go new file mode 100644 index 000000000..0dc80acba --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/query_string.go @@ -0,0 +1,127 @@ +package build + +import ( + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + + "github.com/opentelekomcloud/gophertelekomcloud/internal/multierr" +) + +/* +QueryString is an internal function to be used by request methods in +individual resource packages. + +It accepts a tagged structure and expands it into a URL struct. Field names are +converted into query parameters based on a "q" tag. For example: + + type QueryStruct struct { + Bar string `q:"x_bar"` + Baz int `q:"lorem_ipsum"` + } + + instance := QueryStruct{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into "?x_bar=AAA&lorem_ipsum=BBB". + +The struct's fields may be strings, integers, or boolean values. Fields left at +their type's zero value will be omitted from the query. +*/ +func QueryString(opts interface{}) (*url.URL, error) { + if opts == nil { + return nil, fmt.Errorf("error building query string: %w", ErrNilOpts) + } + + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + params := url.Values{} + + if optsValue.Kind() != reflect.Struct { + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("error building query string: options type is not a struct") + } + + mErr := multierr.MultiError{} + + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + field := optsType.Field(i) + + // Otherwise, the field is not set. + // We duplicate the check from ValidateTags to avoid double reflect package usage + // TODO: investigate performance difference when using ValidateTags + if v.IsZero() { + if structFieldRequired(field) { + // And the field is required. Return an error. + mErr = append(mErr, fmt.Errorf("required query parameter [%s] not set", field.Name)) + } + continue // skip empty fields + } + + if v.Kind() == reflect.Ptr { + v = v.Elem() + } + + // if the field is set, add it to the slice of query pieces + qTag := field.Tag.Get("q") + + // if the field has a 'q' tag, it goes in the query string + if qTag == "" { + continue + } + + tags := strings.Split(qTag, ",") + + switch v.Kind() { + case reflect.String: + params.Add(tags[0], v.String()) + case reflect.Int, reflect.Int32, reflect.Int64: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(tags[0], strconv.FormatBool(v.Bool())) + case reflect.Slice: + switch v.Type().Elem() { + case reflect.TypeOf(0): + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) + } + default: + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], v.Index(i).String()) + } + } + case reflect.Map: + keyKind := v.Type().Key().Kind() + valueKind := v.Type().Elem().Kind() + if keyKind == reflect.String && valueKind == reflect.String { + var s []string + for _, k := range v.MapKeys() { + value := v.MapIndex(k).String() + s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value)) + } + params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", "))) + } else { + mErr = append(mErr, fmt.Errorf("expected map[string]string, got map[%s]%s", keyKind, valueKind)) + } + } + } + + if err := mErr.ErrorOrNil(); err != nil { + return nil, fmt.Errorf("error building query string: %w", err) + } + + return &url.URL{RawQuery: params.Encode()}, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/request_body.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/request_body.go new file mode 100644 index 000000000..21b5dd433 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/request_body.go @@ -0,0 +1,92 @@ +package build + +import ( + "encoding/json" + "fmt" +) + +// Body wraps original request data providing root tag support. +// +// Example: +// +// wrapped := structure { +// Field string `json:"field"` +// } {"data"} +// +// Body{ +// RootTag: "root", +// Wrapped: wrapped, +// } +// +// Will produce the following json: +// +// { +// "root": {"field": "data"} +// } +type Body struct { + RootTag string + Wrapped interface{} +} + +// prepared returns request body, wrapped into a root tag, if required. +func (r Body) prepared() interface{} { + if r.RootTag == "" { + return r.Wrapped + } + + return map[string]interface{}{ + r.RootTag: r.Wrapped, + } +} + +// MarshalJSON satisfies json.Marshaler interface. +func (r Body) MarshalJSON() ([]byte, error) { + return json.Marshal(r.prepared()) +} + +// String allows simple pretty-print of prepared value. +func (r Body) String() string { + jsonData, err := json.MarshalIndent(r.prepared(), "", " ") + if err != nil { + return fmt.Sprintf("!err: %s", err.Error()) + } + + return string(jsonData) +} + +// RequestBody validates given structure by its tags and build the body ready to be marshalled to the JSON. +func RequestBody(opts interface{}, parent string) (*Body, error) { + if opts == nil { + return nil, fmt.Errorf("error building request body: %w", ErrNilOpts) + } + + if err := ValidateTags(opts); err != nil { + return nil, fmt.Errorf("error building request body: %w", err) + } + + return &Body{ + RootTag: parent, + Wrapped: opts, + }, nil +} + +func (r Body) ToMap() (map[string]interface{}, error) { + var res map[string]interface{} + + marshal, err := r.MarshalJSON() + if err != nil { + return nil, err + } + + err = json.Unmarshal(marshal, &res) + return res, err +} + +func RequestBodyMap(opts interface{}, parent string) (map[string]interface{}, error) { + body, err := RequestBody(opts, parent) + if err != nil { + return nil, err + } + + return body.ToMap() +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/tags.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/tags.go new file mode 100644 index 000000000..11a70409b --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/build/tags.go @@ -0,0 +1,98 @@ +package build + +import ( + "fmt" + "reflect" + + "github.com/opentelekomcloud/gophertelekomcloud/internal/multierr" +) + +type fieldValue struct { + // Name of the field in the type. + Name string + // Value of the field in the structure. + Value reflect.Value + // `required` tag value. + TagRequired bool + // `xor` tag value. + TagXOR string + // `or` tag value. + TagOR string +} + +// ValidateTags validating structure by tags. +// +// Supported validations: +// +// required (`required:"true"`) - mark field required, returns error if it is empty. +// or: (`or:"OtherField"`) - requires at least one field to be not empty. +// xor: (`xor:"OtherField"`) - requires exactly of this and the other field to be set. +func ValidateTags(opts interface{}) error { + if opts == nil { + return nil // nil is an ideal value + } + + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + fields := make(map[string]fieldValue) + + if optsValue.Kind() != reflect.Struct { + return nil // no need to go deep + } + + // fill the structure fields map + for i := 0; i < optsValue.NumField(); i++ { + value := optsValue.Field(i) + field := optsType.Field(i) + + fields[field.Name] = fieldValue{ + Name: field.Name, + Value: value, + TagRequired: structFieldRequired(field), + TagXOR: field.Tag.Get("xor"), + TagOR: field.Tag.Get("or"), + } + } + + errors := multierr.MultiError{} + + for name, field := range fields { + fieldErrors := make([]error, 0) + + if field.TagRequired && field.Value.IsZero() { + fieldErrors = append(fieldErrors, + fmt.Errorf("missing input for argument [%s]", name), + ) + } + + orField := field.TagOR + if orField != "" && field.Value.IsZero() && fields[orField].Value.IsZero() { + fieldErrors = append(fieldErrors, + fmt.Errorf("at least one of %s and %s must be provided", name, orField), + ) + } + + xorField := field.TagXOR + if xorField != "" && (field.Value.IsZero() == fields[xorField].Value.IsZero()) { + fieldErrors = append(fieldErrors, + fmt.Errorf("exactly one of %s and %s must be provided", name, xorField), + ) + } + + errors = append(errors, fieldErrors...) + } + + return errors.ErrorOrNil() +} + +func structFieldRequired(field reflect.StructField) bool { + return field.Tag.Get("required") == "true" +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/doc.go new file mode 100644 index 000000000..76558297d --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/doc.go @@ -0,0 +1,3 @@ +// Package extract contains functions for extracting JSON results into given structure or slice pointers. +// Those are wrappers over `json.Marshal` and `json.Unmarshal` functions with additional validation built it +package extract diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/json.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/json.go new file mode 100644 index 000000000..6b5def92d --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/extract/json.go @@ -0,0 +1,164 @@ +package extract + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "reflect" +) + +func intoPtr(body io.Reader, to interface{}, label string) error { + if label == "" { + return Into(body, &to) + } + + var m map[string]interface{} + err := Into(body, &m) + if err != nil { + return err + } + + b, err := JsonMarshal(m[label]) + if err != nil { + return err + } + + toValue := reflect.ValueOf(to) + if toValue.Kind() == reflect.Ptr { + toValue = toValue.Elem() + } + + switch toValue.Kind() { + case reflect.Slice: + typeOfV := toValue.Type().Elem() + if typeOfV.Kind() == reflect.Struct { + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + newSlice := reflect.MakeSlice(reflect.SliceOf(typeOfV), 0, 0) + + for _, v := range m[label].([]interface{}) { + // For each iteration of the slice, we create a new struct. + // This is to work around a bug where elements of a slice + // are reused and not overwritten when the same copy of the + // struct is used: + // + // https://github.com/golang/go/issues/21092 + // https://github.com/golang/go/issues/24155 + // https://play.golang.org/p/NHo3ywlPZli + newType := reflect.New(typeOfV).Elem() + + b, err := JsonMarshal(v) + if err != nil { + return err + } + + // This is needed for structs with an UnmarshalJSON method. + // Technically this is just unmarshalling the response into + // a struct that is never used, but it's good enough to + // trigger the UnmarshalJSON method. + for i := 0; i < newType.NumField(); i++ { + s := newType.Field(i).Addr().Interface() + + // Unmarshal is used rather than NewDecoder to also work + // around the above-mentioned bug. + err = json.Unmarshal(b, s) + if err != nil { + continue + } + } + + newSlice = reflect.Append(newSlice, newType) + } + + // "to" should now be properly modeled to receive the + // JSON response body and unmarshal into all the correct + // fields of the struct or composed extension struct + // at the end of this method. + toValue.Set(newSlice) + } + } + case reflect.Struct: + typeOfV := toValue.Type() + if typeOfV.NumField() > 0 && typeOfV.Field(0).Anonymous { + for i := 0; i < toValue.NumField(); i++ { + toField := toValue.Field(i) + if toField.Kind() == reflect.Struct { + s := toField.Addr().Interface() + err = json.NewDecoder(bytes.NewReader(b)).Decode(s) + if err != nil { + return err + } + } + } + } + } + + err = json.Unmarshal(b, &to) + return err +} + +// JsonMarshal marshals input to bytes via buffer with disabled HTML escaping. +func JsonMarshal(t interface{}) ([]byte, error) { + buffer := &bytes.Buffer{} + enc := json.NewEncoder(buffer) + enc.SetEscapeHTML(false) + err := enc.Encode(t) + return buffer.Bytes(), err +} + +// Into parses input as JSON and convert to a structure. +func Into(body io.Reader, to interface{}) error { + if closer, ok := body.(io.ReadCloser); ok { + defer closer.Close() + } + + byteBody, err := io.ReadAll(body) + if err != nil { + return fmt.Errorf("error reading from stream: %w", err) + } + + if len(byteBody) == 0 { + return nil // empty body - nothing to extract + } + + err = json.Unmarshal(byteBody, to) + if err != nil && !errors.Is(err, io.EOF) { + return fmt.Errorf("error extracting %s into %T: %w", byteBody, to, err) + } + + return nil +} + +func typeCheck(to interface{}, kind reflect.Kind) error { + t := reflect.TypeOf(to) + if k := t.Kind(); k != reflect.Ptr { + return fmt.Errorf("expected pointer, got %v", k) + } + + if kind != t.Elem().Kind() { + return fmt.Errorf("expected pointer to %v, got: %v", kind.String(), t) + } + + return nil +} + +// IntoStructPtr will unmarshal the given body into the provided Struct. +func IntoStructPtr(body io.Reader, to interface{}, label string) error { + err := typeCheck(to, reflect.Struct) + if err != nil { + return err + } + + return intoPtr(body, to, label) +} + +// IntoSlicePtr will unmarshal the provided body into the provided Slice. +func IntoSlicePtr(body io.Reader, to interface{}, label string) error { + err := typeCheck(to, reflect.Slice) + if err != nil { + return err + } + + return intoPtr(body, to, label) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/multierr/multierr.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/multierr/multierr.go new file mode 100644 index 000000000..b53a756eb --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/internal/multierr/multierr.go @@ -0,0 +1,57 @@ +// Package multierr contains simple implementation of multiple error handling. +// Inspired by https://github.com/hashicorp/go-multierror. +package multierr + +import ( + "errors" + "fmt" + "strings" +) + +// MultiError is container for multiple errors. +type MultiError []error + +// Error returns an error message. +func (m MultiError) Error() string { + errorMessages := make([]string, 0, len(m)) + + for _, err := range m { + // can contain nil errors + if err != nil { + errorMessages = append(errorMessages, err.Error()) + } + } + + switch len(errorMessages) { + case 0: + return "" + case 1: + return errorMessages[0] + default: + return fmt.Sprintf("multiple errors returned:\n\t%s", strings.Join(errorMessages, ",\n\t")) + } +} + +// ErrorOrNil returns nil in case there are no errors inside. +func (m MultiError) ErrorOrNil() error { + if m.Error() == "" { + return nil + } + + return m +} + +// Is validates whenever any of included errors Is target error. +func (m MultiError) Is(target error) bool { + if target == nil { + return false + } + + for _, err := range m { + if errors.Is(err, target) { + return true + } + } + + return false +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/auth_env.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/auth_env.go new file mode 100644 index 000000000..53507f32d --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/auth_env.go @@ -0,0 +1,50 @@ +package openstack + +import ( + "github.com/opentelekomcloud/gophertelekomcloud" +) + +/* +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, OS_TENANT_ID, and OS_TENANT_NAME. + +Of these, OS_USERNAME, OS_PASSWORD, and OS_AUTH_URL must have settings, +or an error will result. OS_TENANT_ID, OS_TENANT_NAME, OS_PROJECT_ID, and +OS_PROJECT_NAME are optional. + +OS_TENANT_ID and OS_TENANT_NAME are mutually exclusive to OS_PROJECT_ID and +OS_PROJECT_NAME. If OS_PROJECT_ID and OS_PROJECT_NAME are set, they will +still be referred as "tenant" in Gophercloud. + +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(opts) +*/ +func AuthOptionsFromEnv(envs ...*Env) (golangsdk.AuthOptions, error) { + e := NewEnv(defaultPrefix) + if len(envs) > 0 { + e = envs[0] + } + + ao := golangsdk.AuthOptions{ + IdentityEndpoint: e.GetEnv("AUTH_URL"), + Username: e.GetEnv("USERNAME"), + UserID: e.GetEnv("USERID", "USER_ID"), + Password: e.GetEnv("PASSWORD"), + DomainID: e.GetEnv("DOMAIN_ID", "USER_DOMAIN_ID", "PROJECT_DOMAIN_ID"), + DomainName: e.GetEnv("DOMAIN_NAME", "USER_DOMAIN_NAME", "PROJECT_DOMAIN_NAME"), + TenantID: e.GetEnv("PROJECT_ID", "TENANT_ID"), + TenantName: e.GetEnv("PROJECT_NAME", "TENANT_NAME"), + TokenID: e.GetEnv("TOKEN", "TOKEN_ID"), + AgencyName: e.GetEnv("AGENCY_NAME", "TARGET_AGENCY_NAME"), + AgencyDomainName: e.GetEnv("AGENCY_DOMAIN_NAME", "TARGET_DOMAIN_NAME"), + DelegatedProject: e.GetEnv("DELEGATED_PROJECT", "TARGET_DOMAIN_NAME"), + Passcode: e.GetEnv("PASSCODE"), + } + return ao, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/doc.go new file mode 100644 index 000000000..504375363 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/doc.go @@ -0,0 +1,55 @@ +/* +Package Clusters enables management and retrieval of Clusters +CCE service. + +Example to List Clusters + + listOpts:=clusters.ListOpts{} + allClusters,err:=clusters.List(client,listOpts) + if err != nil { + panic(err) + } + + for _, cluster := range allClusters { + fmt.Printf("%+v\n", cluster) + } + +Example to Create a cluster + + createOpts:=clusters.CreateOpts{Kind:"Cluster", + ApiVersion:"v3", + Metadata:clusters.CreateMetaData{Name:"test-cluster"}, + Spec:clusters.Spec{Type: "VirtualMachine", + Flavor: "cce.s1.small", + Version:"v1.7.3-r10", + HostNetwork:clusters.HostNetworkSpec{VpcId:"3b9740a0-b44d-48f0-84ee-42eb166e54f7", + SubnetId:"3e8e5957-649f-477b-9e5b-f1f75b21c045",}, + ContainerNetwork:clusters.ContainerNetworkSpec{Mode:"overlay_l2"}, + }, + } + cluster,err := clusters.Create(client,createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a cluster + + updateOpts := clusters.UpdateOpts{Spec:clusters.UpdateSpec{Description:"test"}} + + clusterID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + cluster,err := clusters.Update(client,clusterID,updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a cluster + + clusterID := "4e8e5957-649f-477b-9e5b-f1f75b21c03c" + + err := clusters.Delete(client,clusterID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package clusters diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/requests.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/requests.go new file mode 100644 index 000000000..06bd0ea5e --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/requests.go @@ -0,0 +1,283 @@ +package clusters + +import ( + "reflect" + + "github.com/opentelekomcloud/gophertelekomcloud" +) + +var RequestOpts = golangsdk.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/json"}, +} + +// ListOpts allows the filtering of list data using given parameters. +type ListOpts struct { + Name string `json:"name"` + ID string `json:"uuid"` + Type string `json:"type"` + VpcID string `json:"vpc"` + Phase string `json:"phase"` +} + +// List returns collection of clusters. +func List(client *golangsdk.ServiceClient, opts ListOpts) ([]Clusters, error) { + var r ListResult + _, r.Err = client.Get(rootURL(client), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + + allClusters, err := r.ExtractClusters() + if err != nil { + return nil, err + } + + return FilterClusters(allClusters, opts), nil +} + +func FilterClusters(clusters []Clusters, opts ListOpts) []Clusters { + + var refinedClusters []Clusters + var matched bool + m := map[string]FilterStruct{} + + if opts.Name != "" { + m["Name"] = FilterStruct{Value: opts.Name, Driller: []string{"Metadata"}} + } + if opts.ID != "" { + m["Id"] = FilterStruct{Value: opts.ID, Driller: []string{"Metadata"}} + } + if opts.Type != "" { + m["Type"] = FilterStruct{Value: opts.Type, Driller: []string{"Spec"}} + } + if opts.VpcID != "" { + m["VpcId"] = FilterStruct{Value: opts.VpcID, Driller: []string{"Spec", "HostNetwork"}} + } + if opts.Phase != "" { + m["Phase"] = FilterStruct{Value: opts.Phase, Driller: []string{"Status"}} + } + + if len(m) > 0 && len(clusters) > 0 { + for _, cluster := range clusters { + matched = true + + for key, value := range m { + if sVal := GetStructNestedField(&cluster, key, value.Driller); !(sVal == value.Value) { + matched = false + } + } + if matched { + refinedClusters = append(refinedClusters, cluster) + } + } + + } else { + refinedClusters = clusters + } + + return refinedClusters +} + +type FilterStruct struct { + Value string + Driller []string +} + +func GetStructNestedField(v *Clusters, field string, structDriller []string) string { + r := reflect.ValueOf(v) + for _, drillField := range structDriller { + f := reflect.Indirect(r).FieldByName(drillField).Interface() + r = reflect.ValueOf(f) + } + f1 := reflect.Indirect(r).FieldByName(field) + return f1.String() +} + +// CreateOptsBuilder allows extensions to add additional parameters to the +// Create request. +type CreateOptsBuilder interface { + ToClusterCreateMap() (map[string]interface{}, error) +} + +// CreateOpts contains all the values needed to create a new cluster +type CreateOpts struct { + // API type, fixed value Cluster + Kind string `json:"kind" required:"true"` + // API version, fixed value v3 + ApiVersion string `json:"apiversion" required:"true"` + // Metadata required to create a cluster + Metadata CreateMetaData `json:"metadata" required:"true"` + // specifications to create a cluster + Spec Spec `json:"spec" required:"true"` +} + +// Metadata required to create a cluster +type CreateMetaData struct { + // Cluster unique name + Name string `json:"name" required:"true"` + // Cluster tag, key/value pair format + Labels map[string]string `json:"labels,omitempty"` + // Cluster annotation, key/value pair format + Annotations map[string]string `json:"annotations,omitempty"` +} + +// ToClusterCreateMap builds a create request body from CreateOpts. +func (opts CreateOpts) ToClusterCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +type ExpirationOptsBuilder interface { + ToExpirationGetMap() (map[string]interface{}, error) +} + +type ExpirationOpts struct { + Duration int `json:"duration" required:"true"` +} + +func (opts ExpirationOpts) ToExpirationGetMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Create accepts a CreateOpts struct and uses the values to create a new +// logical cluster. +func Create(c *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToClusterCreateMap() + if err != nil { + r.Err = err + return + } + reqOpt := &golangsdk.RequestOpts{OkCodes: []int{201}} + _, r.Err = c.Post(rootURL(c), b, &r.Body, reqOpt) + return +} + +// Get retrieves a particular cluster based on its unique ID. +func Get(c *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = c.Get(resourceURL(c, id), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} + +// GetCert retrieves a particular cluster certificate based on its unique ID. +func GetCert(c *golangsdk.ServiceClient, id string) (r GetCertResult) { + _, r.Err = c.Get(certificateURL(c, id), &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, + }) + return +} + +// GetCertWithExpiration retrieves a particular cluster certificate based on its unique ID. +func GetCertWithExpiration(c *golangsdk.ServiceClient, id string, opts ExpirationOptsBuilder) (r GetCertResult) { + b, err := opts.ToExpirationGetMap() + if err != nil { + r.Err = err + return + } + + _, r.Err = c.Post(certificateURL(c, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, + }) + return +} + +// UpdateOpts contains all the values needed to update a new cluster +type UpdateOpts struct { + Spec UpdateSpec `json:"spec" required:"true"` +} + +type UpdateSpec struct { + // Cluster description + Description string `json:"description,omitempty"` +} + +// UpdateOptsBuilder allows extensions to add additional parameters to the +// Update request. +type UpdateOptsBuilder interface { + ToClusterUpdateMap() (map[string]interface{}, error) +} + +// ToClusterUpdateMap builds an update body based on UpdateOpts. +func (opts UpdateOpts) ToClusterUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "") +} + +// Update allows clusters to update description. +func Update(c *golangsdk.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToClusterUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(resourceURL(c, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// Delete will permanently delete a particular cluster based on its unique ID. +func Delete(c *golangsdk.ServiceClient, id string) (r DeleteResult) { + _, r.Err = c.Delete(resourceURL(c, id), &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return +} + +type DeleteOpts struct { + ErrorStatus string `q:"errorStatus"` + DeleteEfs string `q:"delete_efs"` + DeleteENI string `q:"delete_eni"` + DeleteEvs string `q:"delete_evs"` + DeleteNet string `q:"delete_net"` + DeleteObs string `q:"delete_obs"` + DeleteSfs string `q:"delete_sfs"` +} + +func DeleteWithOpts(c *golangsdk.ServiceClient, id string, opts DeleteOpts) error { + url := resourceURL(c, id) + q, err := golangsdk.BuildQueryString(&opts) + if err != nil { + return err + } + + _, err = c.Delete(url+q.String(), &golangsdk.RequestOpts{ + OkCodes: []int{200}, + MoreHeaders: RequestOpts.MoreHeaders, JSONBody: nil, + }) + return err +} + +type UpdateIpOpts struct { + Action string `json:"action" required:"true"` + Spec IpSpec `json:"spec,omitempty"` + ElasticIp string `json:"elasticIp"` +} + +type IpSpec struct { + ID string `json:"id" required:"true"` +} + +type UpdateIpOptsBuilder interface { + ToMasterIpUpdateMap() (map[string]interface{}, error) +} + +func (opts UpdateIpOpts) ToMasterIpUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "spec") +} + +// Update the access information of a specified cluster. +func UpdateMasterIp(c *golangsdk.ServiceClient, id string, opts UpdateIpOptsBuilder) (r UpdateIpResult) { + b, err := opts.ToMasterIpUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = c.Put(masterIpURL(c, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/results.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/results.go new file mode 100644 index 000000000..328a8a787 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/results.go @@ -0,0 +1,318 @@ +package clusters + +import ( + "encoding/json" + + "github.com/opentelekomcloud/gophertelekomcloud" +) + +type ListCluster struct { + // API type, fixed value Cluster + Kind string `json:"kind"` + // API version, fixed value v3 + ApiVersion string `json:"apiVersion"` + // all Clusters + Clusters []Clusters `json:"items"` +} + +type Clusters struct { + // API type, fixed value Cluster + Kind string `json:"kind" required:"true"` + // API version, fixed value v3 + ApiVersion string `json:"apiversion" required:"true"` + // Metadata of a Cluster + Metadata MetaData `json:"metadata" required:"true"` + // specifications of a Cluster + Spec Spec `json:"spec" required:"true"` + // status of a Cluster + Status Status `json:"status"` +} + +// Metadata required to create a cluster +type MetaData struct { + // Cluster unique name + Name string `json:"name"` + // Cluster unique Id + Id string `json:"uid"` + // Cluster tag, key/value pair format + Labels map[string]string `json:"labels,omitempty"` + // Cluster annotation, key/value pair format + Annotations map[string]string `json:"annotations,omitempty"` +} + +// Specifications to create a cluster +type Spec struct { + // Cluster category: CCE, Turbo + Category string `json:"category,omitempty"` + // Cluster Type: VirtualMachine, BareMetal, or Windows + Type string `json:"type" required:"true"` + // Cluster specifications + Flavor string `json:"flavor" required:"true"` + // Cluster's baseline Kubernetes version. The latest version is recommended + Version string `json:"version,omitempty"` + // Cluster description + Description string `json:"description,omitempty"` + // Public IP ID + PublicIP string `json:"publicip_id,omitempty"` + // Node network parameters + HostNetwork HostNetworkSpec `json:"hostNetwork" required:"true"` + // Container network parameters + ContainerNetwork ContainerNetworkSpec `json:"containerNetwork" required:"true"` + // ENI network parameters + EniNetwork *EniNetworkSpec `json:"eniNetwork,omitempty"` + // Authentication parameters + Authentication AuthenticationSpec `json:"authentication,omitempty"` + // Charging mode of the cluster, which is 0 (on demand) + BillingMode int `json:"billingMode,omitempty"` + // Extended parameter for a cluster + ExtendParam map[string]string `json:"extendParam,omitempty"` + // KubernetesSvcIpRange Service CIDR block or the IP address range which the kubernetes clusterIp must fall within. + // This parameter is available only for clusters of v1.11.7 and later. + KubernetesSvcIpRange string `json:"kubernetesSvcIpRange,omitempty"` + // KubeProxyMode Service forwarding mode. One of `iptables`, `ipvs` + KubeProxyMode string `json:"kubeProxyMode,omitempty"` +} + +// Node network parameters +type HostNetworkSpec struct { + // The ID of the VPC used to create the node + VpcId string `json:"vpc" required:"true"` + // The ID of the subnet used to create the node + SubnetId string `json:"subnet" required:"true"` + // The ID of the high speed network used to create bare metal nodes. + // This parameter is required when creating a bare metal cluster. + HighwaySubnet string `json:"highwaySubnet,omitempty"` + // The ID of the Security Group used to create the node + SecurityGroup string `json:"SecurityGroup,omitempty"` +} + +// Container network parameters +type ContainerNetworkSpec struct { + // Container network type: overlay_l2 , underlay_ipvlan or vpc-router + Mode string `json:"mode" required:"true"` + // Container network segment: 172.16.0.0/16 ~ 172.31.0.0/16. If there is a network segment conflict, it will be automatically reselected. + Cidr string `json:"cidr,omitempty"` +} + +type EniNetworkSpec struct { + // Eni network subnet id + SubnetId string `json:"eniSubnetId" required:"true"` + // Eni network cidr + Cidr string `json:"eniSubnetCIDR" required:"true"` +} + +// Authentication parameters +type AuthenticationSpec struct { + // Authentication mode: rbac , x509 or authenticating_proxy + Mode string `json:"mode" required:"true"` + AuthenticatingProxy map[string]string `json:"authenticatingProxy" required:"true"` +} + +type Status struct { + // The state of the cluster + Phase string `json:"phase"` + // The ID of the Job that is operating asynchronously in the cluster + JobID string `json:"jobID"` + // Reasons for the cluster to become current + Reason string `json:"reason"` + // The status of each component in the cluster + Conditions Conditions `json:"conditions"` + // Kube-apiserver access address in the cluster + Endpoints []Endpoints `json:"-"` +} + +type Conditions struct { + // The type of component + Type string `json:"type"` + // The state of the component + Status string `json:"status"` + // The reason that the component becomes current + Reason string `json:"reason"` +} + +type Endpoints struct { + // The address accessed within the user's subnet - OpenTelekomCloud + Url string `json:"url"` + // Public network access address - OpenTelekomCloud + Type string `json:"type"` + // Internal network address - OTC + Internal string `json:"internal"` + // External network address - OTC + External string `json:"external"` + // Endpoint of the cluster to be accessed through API Gateway - OTC + ExternalOTC string `json:"external_otc"` +} + +type Certificate struct { + // API type, fixed value Config + Kind string `json:"kind"` + // API version, fixed value v1 + ApiVersion string `json:"apiVersion"` + // Cluster list + Clusters []CertClusters `json:"clusters"` + // User list + Users []CertUsers `json:"users"` + // Context list + Contexts []CertContexts `json:"contexts"` + // The current context + CurrentContext string `json:"current-context"` +} + +type CertClusters struct { + // Cluster name + Name string `json:"name"` + // Cluster information + Cluster CertCluster `json:"cluster"` +} + +type CertCluster struct { + // Server IP address + Server string `json:"server"` + // Certificate data + CertAuthorityData string `json:"certificate-authority-data"` +} + +type CertUsers struct { + // User name + Name string `json:"name"` + // Cluster information + User CertUser `json:"user"` +} + +type CertUser struct { + // Client certificate + ClientCertData string `json:"client-certificate-data"` + // Client key data + ClientKeyData string `json:"client-key-data"` +} + +type CertContexts struct { + // Context name + Name string `json:"name"` + // Context information + Context CertContext `json:"context"` +} + +type CertContext struct { + // Cluster name + Cluster string `json:"cluster"` + // User name + User string `json:"user"` +} + +// UnmarshalJSON helps to unmarshal Status fields into needed values. +// OTC and Huawei have different data types and child fields for `endpoints` field in Cluster Status. +// This function handles the unmarshal for both +func (r *Status) UnmarshalJSON(b []byte) error { + type tmp Status + var s struct { + tmp + Endpoints []Endpoints `json:"endpoints"` + } + + err := json.Unmarshal(b, &s) + + if err != nil { + switch err.(type) { + case *json.UnmarshalTypeError: // check if type error occurred (handles the different endpoint structure for huawei and otc) + var s struct { + tmp + Endpoints Endpoints `json:"endpoints"` + } + err := json.Unmarshal(b, &s) + if err != nil { + return err + } + *r = Status(s.tmp) + r.Endpoints = []Endpoints{{Internal: s.Endpoints.Internal, + External: s.Endpoints.External, + ExternalOTC: s.Endpoints.ExternalOTC}} + return nil + default: + return err + } + } + + *r = Status(s.tmp) + r.Endpoints = s.Endpoints + + return err +} + +type commonResult struct { + golangsdk.Result +} + +// Extract is a function that accepts a result and extracts a cluster. +func (r commonResult) Extract() (*Clusters, error) { + var s Clusters + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractCluster is a function that accepts a ListOpts struct, which allows you to filter and sort +// the returned collection for greater efficiency. +func (r commonResult) ExtractClusters() ([]Clusters, error) { + var s ListCluster + err := r.ExtractInto(&s) + if err != nil { + return nil, err + } + + return s.Clusters, nil + +} + +// CreateResult represents the result of a create operation. Call its Extract +// method to interpret it as a Cluster. +type CreateResult struct { + commonResult +} + +// GetResult represents the result of a get operation. Call its Extract +// method to interpret it as a Cluster. +type GetResult struct { + commonResult +} + +// UpdateResult represents the result of an update operation. Call its Extract +// method to interpret it as a Cluster. +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 { + golangsdk.ErrResult +} + +// ListResult represents the result of a list operation. Call its ExtractCluster +// method to interpret it as a Cluster. +type ListResult struct { + commonResult +} + +type GetCertResult struct { + golangsdk.Result +} + +// Extract is a function that accepts a result and extracts a cluster. +func (r GetCertResult) Extract() (*Certificate, error) { + var s Certificate + err := r.ExtractInto(&s) + return &s, err +} + +// ExtractMap is a function that accepts a result and extracts a kubeconfig. +func (r GetCertResult) ExtractMap() (map[string]interface{}, error) { + var s map[string]interface{} + err := r.ExtractInto(&s) + return s, err +} + +// UpdateIpResult represents the result of an update operation. Call its Extract +// method to interpret it as a Cluster. +type UpdateIpResult struct { + golangsdk.ErrResult +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/urls.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/urls.go new file mode 100644 index 000000000..8da26b8a9 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters/urls.go @@ -0,0 +1,25 @@ +package clusters + +import "github.com/opentelekomcloud/gophertelekomcloud" + +const ( + rootPath = "clusters" + certPath = "clustercert" + masterIpPath = "mastereip" +) + +func rootURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL(rootPath) +} + +func resourceURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id) +} + +func certificateURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id, certPath) +} + +func masterIpURL(c *golangsdk.ServiceClient, id string) string { + return c.ServiceURL(rootPath, id, masterIpPath) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/client.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/client.go new file mode 100644 index 000000000..906241073 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/client.go @@ -0,0 +1,970 @@ +package openstack + +import ( + "fmt" + "net/url" + "reflect" + "regexp" + "strings" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects" + tokens3 "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/utils" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +const ( + // 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, golangsdk.EndpointOpts{}) +*/ +func NewClient(endpoint string) (*golangsdk.ProviderClient, error) { + u, err := url.Parse(endpoint) + if err != nil { + return nil, err + } + + u.RawQuery, u.Fragment = "", "" + + var base string + versionRe := regexp.MustCompile("v[0-9.]+/?") + if version := versionRe.FindString(u.Path); version != "" { + base = strings.Replace(u.String(), version, "", -1) + } else { + base = u.String() + } + + endpoint = golangsdk.NormalizeURL(endpoint) + base = golangsdk.NormalizeURL(base) + + p := new(golangsdk.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(ao) + client, err := openstack.NewNetworkV2(client, golangsdk.EndpointOpts{ + Region: utils.GetRegion(ao), + }) +*/ +func AuthenticatedClient(options golangsdk.AuthOptionsProvider) (*golangsdk.ProviderClient, error) { + client, err := NewClient(options.GetIdentityEndpoint()) + if err != nil { + return nil, err + } + + if err := Authenticate(client, options); err != nil { + return nil, err + } + return client, nil +} + +// Authenticate or re-authenticate against the most recent identity service +// supported at the provided endpoint. +func Authenticate(client *golangsdk.ProviderClient, options golangsdk.AuthOptionsProvider) error { + versions := []*utils.Version{ + {ID: v3, Priority: 30, Suffix: "/v3/"}, + } + + chosen, endpoint, err := utils.ChooseVersion(client, versions) + if err != nil { + return err + } + + authOptions, isTokenAuthOptions := options.(golangsdk.AuthOptions) + + if isTokenAuthOptions { + switch chosen.ID { + case v3: + if authOptions.AgencyDomainName != "" && authOptions.AgencyName != "" { + return v3authWithAgency(client, endpoint, &authOptions, golangsdk.EndpointOpts{}) + } + return v3auth(client, endpoint, &authOptions, golangsdk.EndpointOpts{}) + default: + // The switch statement must be out of date from the versions list. + return fmt.Errorf("unrecognized identity version: %s", chosen.ID) + } + } else { + akskAuthOptions, isAkSkOptions := options.(golangsdk.AKSKAuthOptions) + + if isAkSkOptions { + if akskAuthOptions.AgencyDomainName != "" && akskAuthOptions.AgencyName != "" { + return authWithAgencyByAKSK(client, endpoint, akskAuthOptions, golangsdk.EndpointOpts{}) + } + return v3AKSKAuth(client, endpoint, akskAuthOptions, golangsdk.EndpointOpts{}) + + } + return fmt.Errorf("unrecognized auth options provider: %s", reflect.TypeOf(options)) + } +} + +// AuthenticateV3 explicitly authenticates against the identity v3 service. +func AuthenticateV3(client *golangsdk.ProviderClient, options tokens3.AuthOptionsBuilder, eo golangsdk.EndpointOpts) error { + return v3auth(client, "", options, eo) +} + +type token3Result interface { + Extract() (*tokens3.Token, error) + ExtractToken() (*tokens3.Token, error) + ExtractServiceCatalog() (*tokens3.ServiceCatalog, error) + ExtractUser() (*tokens3.User, error) + ExtractRoles() ([]tokens3.Role, error) + ExtractProject() (*tokens3.Project, error) +} + +func v3auth(client *golangsdk.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo golangsdk.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 result token3Result + + if opts.AuthTokenID() != "" { // TODO: Check token validity with Token-By-Token + v3Client.SetToken(opts.AuthTokenID()) + result = tokens3.Get(v3Client, opts.AuthTokenID()) + } else { + result = tokens3.Create(v3Client, opts) + } + + token, err := result.ExtractToken() + if err != nil { + return fmt.Errorf("error extracting token: %w", err) + } + + project, err := result.ExtractProject() + if err != nil { + return fmt.Errorf("error extracting project info: %w", err) + } + + user, err := result.ExtractUser() + if err != nil { + return fmt.Errorf("error extracting user info: %w", err) + } + + serviceCatalog, err := result.ExtractServiceCatalog() + if err != nil { + return fmt.Errorf("error extracting service catalog info: %s", err) + } + + client.TokenID = token.ID + if project != nil { + client.ProjectID = project.ID + client.DomainID = project.Domain.ID + } + if user != nil { + client.UserID = user.ID + client.DomainID = user.Domain.ID + } + + if opts.CanReauth() { + client.ReauthFunc = func() error { + client.TokenID = "" + return v3auth(client, endpoint, opts, eo) + } + } + + clientRegion := "" + if aOpts, ok := opts.(*golangsdk.AuthOptions); ok { + if aOpts.TenantName == "" && project != nil { + aOpts.TenantName = project.Name + } + clientRegion = utils.GetRegion(*aOpts) + } + client.RegionID = clientRegion + + client.EndpointLocator = func(opts golangsdk.EndpointOpts) (string, error) { + // use client region as default one + if opts.Region == "" && clientRegion != "" { + opts.Region = clientRegion + } + return V3EndpointURL(serviceCatalog, opts) + } + + return nil +} + +func v3authWithAgency(client *golangsdk.ProviderClient, endpoint string, opts *golangsdk.AuthOptions, eo golangsdk.EndpointOpts) error { + if opts.TokenID == "" { + err := v3auth(client, endpoint, opts, eo) + if err != nil { + return err + } + } else { + client.TokenID = opts.TokenID + } + + opts1 := golangsdk.AgencyAuthOptions{ + AgencyName: opts.AgencyName, + AgencyDomainName: opts.AgencyDomainName, + DelegatedProject: opts.DelegatedProject, + } + + return v3auth(client, endpoint, &opts1, eo) +} + +func getProjectID(client *golangsdk.ServiceClient, name string) (string, error) { + opts := projects.ListOpts{ + Name: name, + } + allPages, err := projects.List(client, opts).AllPages() + if err != nil { + return "", err + } + + extractProjects, err := projects.ExtractProjects(allPages) + + if err != nil { + return "", err + } + + if len(extractProjects) < 1 { + return "", fmt.Errorf("[DEBUG] cannot find the tenant: %s", name) + } + + return extractProjects[0].ID, nil +} + +func v3AKSKAuth(client *golangsdk.ProviderClient, endpoint string, options golangsdk.AKSKAuthOptions, eo golangsdk.EndpointOpts) error { + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + // Override the generated service endpoint with the one returned by the version endpoint. + if endpoint != "" { + v3Client.Endpoint = endpoint + } + + // update AKSKAuthOptions of ProviderClient + // ProviderClient(client) is a reference to the ServiceClient(v3Client) + defer func() { + client.AKSKAuthOptions.ProjectId = options.ProjectId + client.AKSKAuthOptions.DomainID = options.DomainID + }() + + client.AKSKAuthOptions = options + client.AKSKAuthOptions.DomainID = "" + + if options.ProjectId == "" && options.ProjectName != "" { + id, err := getProjectID(v3Client, options.ProjectName) + if err != nil { + return err + } + options.ProjectId = id + client.AKSKAuthOptions.ProjectId = options.ProjectId + } + + if options.DomainID == "" && options.Domain != "" { + id, err := getDomainID(options.Domain, v3Client) + if err != nil { + options.DomainID = "" + } else { + options.DomainID = id + } + } + + if options.BssDomainID == "" && options.BssDomain != "" { + id, err := getDomainID(options.BssDomain, v3Client) + if err != nil { + options.BssDomainID = "" + } else { + options.BssDomainID = id + } + } + + client.ProjectID = options.ProjectId + client.DomainID = options.BssDomainID + + var entries = make([]tokens3.CatalogEntry, 0, 1) + err = catalog.List(v3Client).EachPage(func(page pagination.Page) (bool, error) { + catalogList, err := catalog.ExtractServiceCatalog(page) + if err != nil { + return false, err + } + + entries = append(entries, catalogList...) + + return true, nil + }) + + if err != nil { + return err + } + clientRegion := utils.GetRegionFromAKSK(options) + client.RegionID = clientRegion + + client.EndpointLocator = func(opts golangsdk.EndpointOpts) (string, error) { + return V3EndpointURL(&tokens3.ServiceCatalog{ + Entries: entries, + }, opts) + } + return nil +} + +func authWithAgencyByAKSK(client *golangsdk.ProviderClient, endpoint string, opts golangsdk.AKSKAuthOptions, eo golangsdk.EndpointOpts) error { + err := v3AKSKAuth(client, endpoint, opts, eo) + if err != nil { + return err + } + + v3Client, err := NewIdentityV3(client, eo) + if err != nil { + return err + } + + if v3Client.AKSKAuthOptions.DomainID == "" { + return fmt.Errorf("must config domain name") + } + + opts2 := golangsdk.AgencyAuthOptions{ + AgencyName: opts.AgencyName, + AgencyDomainName: opts.AgencyDomainName, + DelegatedProject: opts.DelegatedProject, + } + result := tokens3.Create(v3Client, &opts2) + token, err := result.ExtractToken() + if err != nil { + return err + } + + project, err := result.ExtractProject() + if err != nil { + return fmt.Errorf("error extracting project info: %s", err) + } + + user, err := result.ExtractUser() + if err != nil { + return fmt.Errorf("error extracting user info: %s", err) + } + + serviceCatalog, err := result.ExtractServiceCatalog() + if err != nil { + return err + } + + client.TokenID = token.ID + if project != nil { + client.ProjectID = project.ID + } + if user != nil { + client.UserID = user.ID + } + + client.ReauthFunc = func() error { + client.TokenID = "" + return authWithAgencyByAKSK(client, endpoint, opts, eo) + } + + client.EndpointLocator = func(opts golangsdk.EndpointOpts) (string, error) { + return V3EndpointURL(serviceCatalog, opts) + } + + client.AKSKAuthOptions.AccessKey = "" + return nil +} + +func getDomainID(name string, client *golangsdk.ServiceClient) (string, error) { + old := client.Endpoint + defer func() { client.Endpoint = old }() + + client.Endpoint = old + "auth/" + + opts := domains.ListOpts{ + Name: name, + } + allPages, err := domains.List(client, &opts).AllPages() + if err != nil { + return "", fmt.Errorf("list domains failed, err=%s", err) + } + + all, err := domains.ExtractDomains(allPages) + if err != nil { + return "", fmt.Errorf("extract domains failed, err=%s", err) + } + + count := len(all) + switch count { + case 0: + err := &golangsdk.ErrResourceNotFound{} + err.ResourceType = "iam" + err.Name = name + return "", err + case 1: + return all[0].ID, nil + default: + err := &golangsdk.ErrMultipleResourcesFound{} + err.ResourceType = "iam" + err.Name = name + err.Count = count + return "", err + } +} + +// NewIdentityV3 creates a ServiceClient that may be used to access the v3 +// identity service. +func NewIdentityV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + endpoint := client.IdentityBase + "v3/" + clientType := "identity" + var err error + if !reflect.DeepEqual(eo, golangsdk.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 and requests will fail unless targeted at /v3. + if !strings.HasSuffix(endpoint, "v3/") { + endpoint = endpoint + "v3/" + } + + return &golangsdk.ServiceClient{ + ProviderClient: client, + Endpoint: endpoint, + Type: clientType, + }, nil +} + +func initClientOpts(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts, clientType string) (*golangsdk.ServiceClient, error) { + sc := new(golangsdk.ServiceClient) + eo.ApplyDefaults(clientType) + locator, err := client.EndpointLocator(eo) + if err != nil { + return sc, err + } + sc.ProviderClient = client + sc.Endpoint = locator + sc.Type = clientType + return sc, nil +} + +// initCommonServiceClient is a workaround for services missing from the catalog. +// Firstly, we initialize a service client by "volumev2" type, the endpoint likes https://evs.{region}.{xxx.com}/v2/{project_id} +// then we replace the endpoint with the specified srv and version. +func initCommonServiceClient(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts, srv string, version string) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "volumev2") + if err != nil { + return nil, err + } + + e := strings.Replace(sc.Endpoint, "v2", version, 1) + sc.Endpoint = strings.Replace(e, "evs", srv, 1) + sc.ResourceBase = sc.Endpoint + return sc, err +} + +// NewComputeV2 creates a ServiceClient that may be used with the v2 compute +// package. +func NewComputeV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "compute") +} + +// NewNetworkV2 creates a ServiceClient that may be used with the v2 network +// package. +func NewNetworkV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1 +// block storage service. +func NewBlockStorageV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "volume") +} + +// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2 +// block storage service. +func NewBlockStorageV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "volumev2") +} + +// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service. +func NewBlockStorageV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "volumev3") +} + +// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service. +func NewSharedFileSystemV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "sharev2") +} + +// NewSharedFileSystemTurboV1 creates a ServiceClient that may be used to access the v2 shared file system turbo service. +func NewSharedFileSystemTurboV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "sfsturbo") +} + +// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1 +// orchestration service. +func NewOrchestrationV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "orchestration") +} + +// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS +// service. +func NewDNSV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dns") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +func NewDWSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dws") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v1.0/" + client.ProjectID + "/" + return sc, err +} + +func NewIMSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} + +func NewIMSV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "image") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v2/" + return sc, err +} + +// NewOtcV1 creates a ServiceClient that may be used with the v1 network package. +func NewElbV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts, otctype string) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "compute") + if err != nil { + return nil, err + } + sc.Endpoint = strings.Replace(strings.Replace(sc.Endpoint, "ecs", otctype, 1), "/v2/", "/v1.0/", 1) + sc.ResourceBase = sc.Endpoint + sc.Type = otctype + return sc, err +} + +func NewCESClient(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "volumev2") + if err != nil { + return nil, err + } + e := strings.Replace(sc.Endpoint, "v2", "V1.0", 1) + sc.Endpoint = strings.Replace(e, "evs", "ces", 1) + sc.ResourceBase = sc.Endpoint + return sc, err +} + +// NewComputeV1 creates a ServiceClient that may be used with the v1 compute +// package. +func NewComputeV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "ecs") +} + +func NewRdsTagV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + if err != nil { + return nil, err + } + sc.Endpoint = strings.Replace(sc.Endpoint, "vpc", "rds", 1) + sc.Endpoint = sc.Endpoint + "v1/" + sc.ResourceBase = sc.Endpoint + client.ProjectID + "/rds/" + return sc, err +} + +// NewAutoScalingV1 creates a ServiceClient that may be used to access the +// auto-scaling service of OpenTelekomCloud public cloud +func NewAutoScalingV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "asv1") +} + +// NewAutoScalingV2 creates a ServiceClient that may be used to access the +// auto-scaling service of OpenTelekomCloud public cloud +func NewAutoScalingV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "asv1") + if err != nil { + return nil, err + } + sc.Endpoint = strings.Replace(sc.Endpoint, "v1", "v2", 1) + return sc, err +} + +// NewNetworkV1 creates a ServiceClient that may be used with the v1 network +// package. +func NewNetworkV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v1/" + return sc, err +} + +// NewVpcEpV1 creates a ServiceClient that may be used with the v1 VPC Endpoint service +func NewVpcEpV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "vpcep") + if err != nil { + return nil, err + } + return sc, err +} + +// NewVpcV3 creates a ServiceClient that may be used with the v3 VPC service +func NewVpcV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "vpc") + if err != nil { + return nil, err + } + sc.Endpoint = strings.Replace(sc.Endpoint, "v1", "v3", 1) + sc.ResourceBase = sc.Endpoint + "vpc/" + return sc, err +} + +// NewNatV2 creates a ServiceClient that may be used with the v2 nat package. +func NewNatV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "nat") +} + +// NewMapReduceV1 creates a ServiceClient that may be used with the v1 MapReduce service. +func NewMapReduceV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "mrs") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + client.ProjectID + "/" + return sc, err +} + +// NewAntiDDoSV1 creates a ServiceClient that may be used with the v1 Anti DDoS Service +// package. +func NewAntiDDoSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "antiddos") +} + +// NewDCaaSV2 creates a ServiceClient that may be used to access the v1 Distributed Message Service. +func NewDCaaSV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "dcaas") +} + +// NewDMSServiceV1 creates a ServiceClient that may be used to access the v1 Distributed Message Service. +func NewDMSServiceV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "dmsv1") +} + +func NewDMSServiceV11(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dmsv1") + if err != nil { + return nil, err + } + sc.Endpoint = strings.Replace(sc.Endpoint, "v1.0", "v1", 1) + return sc, err +} + +// NewDMSServiceV2 creates a ServiceClient that may be used to access the v2 Distributed Message Service. +func NewDMSServiceV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "dmsv2") +} + +// NewDCSServiceV1 creates a ServiceClient that may be used to access the v1 Distributed Cache Service. +func NewDCSServiceV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "network") + if err != nil { + return nil, err + } + sc.Endpoint = strings.Replace(sc.Endpoint, "vpc", "dcs", 1) + sc.ResourceBase = sc.Endpoint + "v1.0/" + client.ProjectID + "/" + return sc, err +} + +// NewDDSServiceV3 creates a ServiceClient that may be used to access the Document Database Service. +func NewDDSServiceV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "ddsv3") + return sc, err +} + +func NewDISServiceV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "dis") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v2/" + client.ProjectID + "/" + return sc, err +} + +// NewDRSServiceV3 creates a ServiceClient that may be used to access the Document Database Service. +func NewDRSServiceV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initCommonServiceClient(client, eo, "drs", "v3") + if err != nil { + return nil, err + } + return sc, nil +} + +// NewOBSService creates a ServiceClient that may be used to access the Object Storage Service. +func NewOBSService(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "object") + return sc, err +} + +// NewDeHServiceV1 creates a ServiceClient that may be used to access the v1 Dedicated Hosts service. +func NewDeHServiceV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "deh") + return sc, err +} + +// NewCSBSService creates a ServiceClient that can be used to access the Cloud Server Backup service. +func NewCSBSService(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "data-protect") + return sc, err +} + +// NewCBRService create a ServiceClient that can be used to access the Cloud Backup and Recovery service. +func NewCBRService(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "cbr") +} + +// NewCSSService creates a ServiceClient that can be used to access the Cloud Search service. +func NewCSSService(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "css") +} + +// NewVBS creates a service client that is used for VBS. +func NewVBS(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "volumev2") + if err != nil { + return nil, err + } + sc.Endpoint = strings.Replace(sc.Endpoint, "evs", "vbs", 1) + sc.ResourceBase = sc.Endpoint + return sc, err +} + +func NewVBSServiceV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "vbsv2") +} + +// NewCTSV1 creates a ServiceClient that can be used to access the Cloud Trace service. +func NewCTSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "cts") + return sc, err +} + +// NewCTSV2 creates a ServiceClient that can be used to access the Cloud Trace service. +func NewCTSV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "cts") + sc.Endpoint = strings.Replace(sc.Endpoint, "v1", "v2", 1) + return sc, err +} + +// NewCTSV3 creates a ServiceClient that can be used to access the Cloud Trace service. +func NewCTSV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "cts") + sc.Endpoint = strings.Replace(sc.Endpoint, "v1.0", "v3", 1) + return sc, err +} + +// NewELBV1 creates a ServiceClient that may be used to access the ELB service. +func NewELBV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "elbv1") + return sc, err +} + +// NewELBV2 creates a ServiceClient that may be used to access the ELBv2 service. +func NewELBV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "elb") + if suf := "v1.0/"; strings.HasSuffix(sc.Endpoint, suf) { + sc.Endpoint = strings.TrimSuffix(sc.Endpoint, suf) + } + sc.ResourceBase = sc.Endpoint + "v2.0/" + return sc, err +} + +// NewELBV3 creates a ServiceClient that may be used to access the ELBv3 service. +func NewELBV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "elbv3") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "elb/" + return sc, nil +} + +// NewRDSV1 creates a ServiceClient that may be used to access the RDS service. +func NewRDSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "rdsv1") + return sc, err +} + +// NewKMSV1 creates a ServiceClient that may be used to access the KMS service. +func NewKMSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "kmsv1") +} + +// NewSMNV2 creates a ServiceClient that may be used to access the SMN service. +func NewSMNV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "smnv2") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "notifications/" + return sc, err +} + +// NewSMNV2Tags creates a ServiceClient that may be used to access the SMN tags service. +func NewSMNV2Tags(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "smnv2") + if err != nil { + return nil, err + } + return sc, err +} + +// NewCCEv1 creates a ServiceClient that may be used to access the CCE k8s service. +func NewCCEv1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "cce") +} + +// NewCCE creates a ServiceClient that may be used to access the CCE service. +func NewCCE(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "ccev2.0") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "api/v3/projects/" + client.ProjectID + "/" + return sc, err +} + +// NewWAFV1 creates a ServiceClient that may be used to access the WAF service. +func NewWAFV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "waf") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v1/" + client.ProjectID + "/waf/" + return sc, err +} + +// NewWAFDSwissV1 creates a ServiceClient that may be used to access the premium WAF service. +func NewWAFDSwissV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "waf") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + "v1/" + client.ProjectID + "/" + return sc, err +} + +// NewWAFDV1 creates a ServiceClient that may be used to access the premium WAF service. +func NewWAFDV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initClientOpts(client, eo, "premium-waf") + if err != nil { + return nil, err + } + sc.ResourceBase = sc.Endpoint + return sc, err +} + +// NewRDSV3 creates a ServiceClient that may be used to access the RDS service. +func NewRDSV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "rdsv3") +} + +// NewSDRSV1 creates a ServiceClient that may be used with the v1 SDRS service. +func NewSDRSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "sdrs") +} + +// NewLTSV2 creates a ServiceClient that may be used to access the LTS service. +func NewLTSV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initCommonServiceClient(client, eo, "lts", "v2") + return sc, err +} + +// NewSWRV2 creates a ServiceClient that may be used to access the SWR service. +func NewSWRV2(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + serviceClient, err := initClientOpts(client, eo, "smn") // SMN is v2 and has no project ID + if err != nil { + return nil, err + } + serviceClient.Endpoint = strings.Replace(serviceClient.Endpoint, "smn", "swr-api", 1) + serviceClient.ResourceBase = serviceClient.Endpoint + return serviceClient, err +} + +// NewTMSV1 creates a ServiceClient that may be used to access the TMS service. +func NewTMSV1(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initClientOpts(client, eo, "tms") +} + +func NewGaussDBV3(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initCommonServiceClient(client, eo, "gaussdb", "mysql/v3") + return sc, err +} + +func NewDataArtsV11(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + return initCommonServiceClient(client, eo, "cdm", "v1.1") +} + +func NewAPIGW(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initCommonServiceClient(client, eo, "apig", "v2") + return sc, err +} + +func NewFuncGraph(client *golangsdk.ProviderClient, eo golangsdk.EndpointOpts) (*golangsdk.ServiceClient, error) { + sc, err := initCommonServiceClient(client, eo, "functiongraph", "v2") + return sc, err +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/doc.go new file mode 100644 index 000000000..bb82d79db --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/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(ao) + client, err := openstack.NewNetworkV2(client, golangsdk.EndpointOpts{ + Region: utils.GetRegion(ao), + }) +*/ +package openstack diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_location.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_location.go new file mode 100644 index 000000000..4bb4ed2ff --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_location.go @@ -0,0 +1,57 @@ +package openstack + +import ( + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + tokens3 "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens" +) + +/* +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 golangsdk.EndpointOpts) (string, error) { + // Extract Endpoints from the catalog entries that match the requested Type, Interface, + // Name if provided, and Region if provided. + var endpoints = make([]tokens3.Endpoint, 0, 1) + for _, entry := range catalog.Entries { + if (entry.Type == opts.Type) && (opts.Name == "" || entry.Name == opts.Name) { + for _, endpoint := range entry.Endpoints { + if opts.Availability != golangsdk.AvailabilityAdmin && + opts.Availability != golangsdk.AvailabilityPublic && + opts.Availability != golangsdk.AvailabilityInternal { + err := &ErrInvalidAvailabilityProvided{} + err.Argument = "Availability" + err.Value = opts.Availability + return "", err + } + if opts.Availability == golangsdk.Availability(endpoint.Interface) && + (opts.Region == "" || endpoint.Region == opts.Region) { + endpoints = append(endpoints, endpoint) + } + } + } + } + + // If multiple endpoints were found, use the first result + // and disregard the other endpoints. + // + // This behavior matches the Python library. See GH-1764. + if len(endpoints) > 1 { + endpoints = endpoints[0:1] + } + + // Extract the URL from the matching Endpoint. + for _, endpoint := range endpoints { + return golangsdk.NormalizeURL(endpoint.URL), nil + } + + // Report an error if there were no matching endpoints. + err := &golangsdk.ErrEndpointNotFound{} + return "", err +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_util.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_util.go new file mode 100644 index 000000000..e5056815f --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/endpoint_util.go @@ -0,0 +1,21 @@ +package openstack + +import ( + "regexp" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" +) + +// A regular expression used to verify whether or not contains a project id in an endpoint url +var endpointProjectIdMatcher = regexp.MustCompile(`http[s]?://.+/(?:[V|v]\d+|[V|v]\d+\.\d+)/([a-z|A-Z|0-9]{32})(?:/|$)`) + +// ContainsProjectId detects whether or not the endpoint url contains a projectID +func ContainsProjectId(endpointUrl string) bool { + return endpointProjectIdMatcher.Match([]byte(endpointUrl)) +} + +func StdRequestOpts() *golangsdk.RequestOpts { + return &golangsdk.RequestOpts{ + MoreHeaders: map[string]string{"Content-Type": "application/json", "X-Language": "en-us"}, + } +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/errors.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/errors.go new file mode 100644 index 000000000..20d0e70f2 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/errors.go @@ -0,0 +1,59 @@ +package openstack + +import ( + "fmt" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" + tokens3 "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens" +) + +// ErrEndpointNotFound is the error when no suitable endpoint can be found +// in the user's catalog +type ErrEndpointNotFound struct{ golangsdk.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{ golangsdk.ErrInvalidInput } + +func (e ErrInvalidAvailabilityProvided) Error() string { + return fmt.Sprintf("Unexpected availability in endpoint query: %s", e.Value) +} + +// ErrMultipleMatchingEndpointsV3 is the error when more than one endpoint +// for the given options is found in the v3 catalog +type ErrMultipleMatchingEndpointsV3 struct { + golangsdk.BaseError + Endpoints []tokens3.Endpoint +} + +func (e ErrMultipleMatchingEndpointsV3) Error() string { + return fmt.Sprintf("Discovered %d matching endpoints: %#v", len(e.Endpoints), e.Endpoints) +} + +// ErrNoAuthURL is the error when the OS_AUTH_URL environment variable is not +// found +type ErrNoAuthURL struct{ golangsdk.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{ golangsdk.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{ golangsdk.ErrInvalidInput } + +func (e ErrNoPassword) Error() string { + return "Environment variable OS_PASSWORD needs to be set." +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/requests.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/requests.go new file mode 100644 index 000000000..0b2318077 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/requests.go @@ -0,0 +1,14 @@ +package catalog + +import ( + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +// List enumerates the services available to a specific user. +func List(client *golangsdk.ServiceClient) pagination.Pager { + url := listURL(client) + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return CatalogPage{pagination.LinkedPageBase{PageResult: r}} + }) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/results.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/results.go new file mode 100644 index 000000000..d5b4c4c94 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/results.go @@ -0,0 +1,27 @@ +package catalog + +import ( + "github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +// CatalogPage is a single page of Service results. +type CatalogPage struct { + pagination.LinkedPageBase +} + +// IsEmpty returns true if the CatalogPage contains no results. +func (p CatalogPage) IsEmpty() (bool, error) { + services, err := ExtractServiceCatalog(p) + return len(services) == 0, err +} + +// ExtractServiceCatalog extracts a slice of Catalog from a Collection acquired +// from List. +func ExtractServiceCatalog(r pagination.Page) ([]tokens.CatalogEntry, error) { + var s struct { + Entries []tokens.CatalogEntry `json:"catalog"` + } + err := (r.(CatalogPage)).ExtractInto(&s) + return s.Entries, err +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/urls.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/urls.go new file mode 100644 index 000000000..21673bf0a --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog/urls.go @@ -0,0 +1,7 @@ +package catalog + +import "github.com/opentelekomcloud/gophertelekomcloud" + +func listURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("auth", "catalog") +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/doc.go new file mode 100644 index 000000000..720db782f --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/doc.go @@ -0,0 +1,59 @@ +/* +Package domains manages and retrieves Domains in the OpenStack Identity Service. + +Example to List Domains + + var iTrue bool = true + listOpts := domains.ListOpts{ + Enabled: &iTrue, + } + + allPages, err := domains.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allDomains, err := domains.ExtractDomains(allPages) + if err != nil { + panic(err) + } + + for _, domain := range allDomains { + fmt.Printf("%+v\n", domain) + } + +Example to Create a Domain + + createOpts := domains.CreateOpts{ + Name: "domain name", + Description: "Test domain", + } + + domain, err := domains.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Domain + + domainID := "0fe36e73809d46aeae6705c39077b1b3" + + var iFalse bool = false + updateOpts := domains.UpdateOpts{ + Enabled: &iFalse, + } + + domain, err := domains.Update(identityClient, domainID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Domain + + domainID := "0fe36e73809d46aeae6705c39077b1b3" + err := domains.Delete(identityClient, domainID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package domains diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/requests.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/requests.go new file mode 100644 index 000000000..24bd27317 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/requests.go @@ -0,0 +1,129 @@ +package domains + +import ( + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToDomainListQuery() (string, error) +} + +// ListOpts provides options to filter the List results. +type ListOpts struct { + // Enabled filters the response by enabled domains. + Enabled *bool `q:"enabled"` + + // Name filters the response by domain name. + Name string `q:"name"` +} + +// ToDomainListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToDomainListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), err +} + +// List enumerates the domains to which the current token has access. +func List(client *golangsdk.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToDomainListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return DomainPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single domain, by ID. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToDomainCreateMap() (map[string]interface{}, error) +} + +// CreateOpts provides options used to create a domain. +type CreateOpts struct { + // Name is the name of the new domain. + Name string `json:"name" required:"true"` + + // Description is a description of the domain. + Description string `json:"description,omitempty"` + + // Enabled sets the domain status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// ToDomainCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToDomainCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "domain") +} + +// Create creates a new Domain. +func Create(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToDomainCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{201}, + }) + return +} + +// Delete deletes a domain. +func Delete(client *golangsdk.ServiceClient, domainID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, domainID), nil) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToDomainUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a domain. +type UpdateOpts struct { + // Name is the name of the domain. + Name string `json:"name,omitempty"` + + // Description is the description of the domain. + Description string `json:"description,omitempty"` + + // Enabled sets the domain status to enabled or disabled. + Enabled *bool `json:"enabled,omitempty"` +} + +// ToUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToDomainUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "domain") +} + +// Update modifies the attributes of a domain. +func Update(client *golangsdk.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToDomainUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/results.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/results.go new file mode 100644 index 000000000..5284644ff --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/results.go @@ -0,0 +1,97 @@ +package domains + +import ( + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +// A Domain is a collection of projects, users, and roles. +type Domain struct { + // Description is the description of the Domain. + Description string `json:"description"` + + // Enabled is whether or not the domain is enabled. + Enabled bool `json:"enabled"` + + // ID is the unique ID of the domain. + ID string `json:"id"` + + // Links contains referencing links to the domain. + Links map[string]interface{} `json:"links"` + + // Name is the name of the domain. + Name string `json:"name"` +} + +type domainResult struct { + golangsdk.Result +} + +// GetResult is the response from a Get operation. Call its Extract method +// to interpret it as a Domain. +type GetResult struct { + domainResult +} + +// CreateResult is the response from a Create operation. Call its Extract method +// to interpret it as a Domain. +type CreateResult struct { + domainResult +} + +// DeleteResult is the response from a Delete operation. Call its ExtractErr to +// determine if the request succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Domain. +type UpdateResult struct { + domainResult +} + +// DomainPage is a single page of Domain results. +type DomainPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Domains contains any results. +func (r DomainPage) IsEmpty() (bool, error) { + domains, err := ExtractDomains(r) + return len(domains) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r DomainPage) 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 +} + +// ExtractDomains returns a slice of Domains contained in a single page of +// results. +func ExtractDomains(r pagination.Page) ([]Domain, error) { + var s struct { + Domains []Domain `json:"domains"` + } + err := (r.(DomainPage)).ExtractInto(&s) + return s.Domains, err +} + +// Extract interprets any domainResults as a Domain. +func (r domainResult) Extract() (*Domain, error) { + var s struct { + Domain *Domain `json:"domain"` + } + err := r.ExtractInto(&s) + return s.Domain, err +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/urls.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/urls.go new file mode 100644 index 000000000..3785386c5 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains/urls.go @@ -0,0 +1,23 @@ +package domains + +import "github.com/opentelekomcloud/gophertelekomcloud" + +func listURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("domains") +} + +func getURL(client *golangsdk.ServiceClient, domainID string) string { + return client.ServiceURL("domains", domainID) +} + +func createURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("domains") +} + +func deleteURL(client *golangsdk.ServiceClient, domainID string) string { + return client.ServiceURL("domains", domainID) +} + +func updateURL(client *golangsdk.ServiceClient, domainID string) string { + return client.ServiceURL("domains", domainID) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/doc.go new file mode 100644 index 000000000..f01980074 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/doc.go @@ -0,0 +1,58 @@ +/* +Package projects manages and retrieves Projects in the OpenStack Identity +Service. + +Example to List Projects + + listOpts := projects.ListOpts{ + Enabled: golangsdk.Enabled, + } + + allPages, err := projects.List(identityClient, listOpts).AllPages() + if err != nil { + panic(err) + } + + allProjects, err := projects.ExtractProjects(allPages) + if err != nil { + panic(err) + } + + for _, project := range allProjects { + fmt.Printf("%+v\n", project) + } + +Example to Create a Project + + createOpts := projects.CreateOpts{ + Name: "project_name", + Description: "Project Description" + } + + project, err := projects.Create(identityClient, createOpts).Extract() + if err != nil { + panic(err) + } + +Example to Update a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + + updateOpts := projects.UpdateOpts{ + Enabled: golangsdk.Disabled, + } + + project, err := projects.Update(identityClient, projectID, updateOpts).Extract() + if err != nil { + panic(err) + } + +Example to Delete a Project + + projectID := "966b3c7d36a24facaf20b7e458bf2192" + err := projects.Delete(identityClient, projectID).ExtractErr() + if err != nil { + panic(err) + } +*/ +package projects diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/requests.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/requests.go new file mode 100644 index 000000000..41ed627a7 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/requests.go @@ -0,0 +1,132 @@ +package projects + +import ( + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +// ListOptsBuilder allows extensions to add additional parameters to +// the List request +type ListOptsBuilder interface { + ToProjectListQuery() (string, error) +} + +// ListOpts enables filtering of a list request. +type ListOpts struct { + // DomainID filters the response by a domain ID. + DomainID string `q:"domain_id"` + + // Name filters the response by project name. + Name string `q:"name"` + + // ParentID filters the response by projects of a given parent project. + ParentID string `q:"parent_id"` +} + +// ToProjectListQuery formats a ListOpts into a query string. +func (opts ListOpts) ToProjectListQuery() (string, error) { + q, err := golangsdk.BuildQueryString(opts) + if err != nil { + return "", err + } + return q.String(), err +} + +// List enumerates the Projects to which the current token has access. +func List(client *golangsdk.ServiceClient, opts ListOptsBuilder) pagination.Pager { + url := listURL(client) + if opts != nil { + query, err := opts.ToProjectListQuery() + if err != nil { + return pagination.Pager{Err: err} + } + url += query + } + return pagination.NewPager(client, url, func(r pagination.PageResult) pagination.Page { + return ProjectPage{pagination.LinkedPageBase{PageResult: r}} + }) +} + +// Get retrieves details on a single project, by ID. +func Get(client *golangsdk.ServiceClient, id string) (r GetResult) { + _, r.Err = client.Get(getURL(client, id), &r.Body, nil) + return +} + +// CreateOptsBuilder allows extensions to add additional parameters to +// the Create request. +type CreateOptsBuilder interface { + ToProjectCreateMap() (map[string]interface{}, error) +} + +// CreateOpts represents parameters used to create a project. +type CreateOpts struct { + // DomainID is the ID this project will belong under. + DomainID string `json:"domain_id,omitempty"` + + // Name is the name of the project. + Name string `json:"name" required:"true"` + + // ParentID specifies the parent project of this new project. + ParentID string `json:"parent_id,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToProjectCreateMap formats a CreateOpts into a create request. +func (opts CreateOpts) ToProjectCreateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "project") +} + +// Create creates a new Project. +func Create(client *golangsdk.ServiceClient, opts CreateOptsBuilder) (r CreateResult) { + b, err := opts.ToProjectCreateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Post(createURL(client), &b, &r.Body, nil) + return +} + +// Delete deletes a project. +func Delete(client *golangsdk.ServiceClient, projectID string) (r DeleteResult) { + _, r.Err = client.Delete(deleteURL(client, projectID), &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} + +// UpdateOptsBuilder allows extensions to add additional parameters to +// the Update request. +type UpdateOptsBuilder interface { + ToProjectUpdateMap() (map[string]interface{}, error) +} + +// UpdateOpts represents parameters to update a project. +type UpdateOpts struct { + // Name is the name of the project. + Name string `json:"name,omitempty"` + + // Description is the description of the project. + Description string `json:"description,omitempty"` +} + +// ToUpdateCreateMap formats a UpdateOpts into an update request. +func (opts UpdateOpts) ToProjectUpdateMap() (map[string]interface{}, error) { + return golangsdk.BuildRequestBody(opts, "project") +} + +// Update modifies the attributes of a project. +func Update(client *golangsdk.ServiceClient, id string, opts UpdateOptsBuilder) (r UpdateResult) { + b, err := opts.ToProjectUpdateMap() + if err != nil { + r.Err = err + return + } + _, r.Err = client.Patch(updateURL(client, id), b, &r.Body, &golangsdk.RequestOpts{ + OkCodes: []int{200}, + }) + return +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/results.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/results.go new file mode 100644 index 000000000..71bb92b9c --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/results.go @@ -0,0 +1,103 @@ +package projects + +import ( + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/pagination" +) + +type projectResult struct { + golangsdk.Result +} + +// GetResult is the result of a Get request. Call its Extract method to +// interpret it as a Project. +type GetResult struct { + projectResult +} + +// CreateResult is the result of a Create request. Call its Extract method to +// interpret it as a Project. +type CreateResult struct { + projectResult +} + +// DeleteResult is the result of a Delete request. Call its ExtractErr method to +// determine if the request succeeded or failed. +type DeleteResult struct { + golangsdk.ErrResult +} + +// UpdateResult is the result of an Update request. Call its Extract method to +// interpret it as a Project. +type UpdateResult struct { + projectResult +} + +// Project represents an OpenStack Identity Project. +type Project struct { + // IsDomain indicates whether the project is a domain. + IsDomain bool `json:"is_domain"` + + // Description is the description of the project. + Description string `json:"description"` + + // DomainID is the domain ID the project belongs to. + DomainID string `json:"domain_id"` + + // Enabled is whether or not the project is enabled. + Enabled bool `json:"enabled"` + + // ID is the unique ID of the project. + ID string `json:"id"` + + // Name is the name of the project. + Name string `json:"name"` + + // ParentID is the parent_id of the project. + ParentID string `json:"parent_id"` +} + +// ProjectPage is a single page of Project results. +type ProjectPage struct { + pagination.LinkedPageBase +} + +// IsEmpty determines whether or not a page of Projects contains any results. +func (r ProjectPage) IsEmpty() (bool, error) { + projects, err := ExtractProjects(r) + return len(projects) == 0, err +} + +// NextPageURL extracts the "next" link from the links section of the result. +func (r ProjectPage) 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 +} + +// ExtractProjects returns a slice of Projects contained in a single page of +// results. +func ExtractProjects(r pagination.Page) ([]Project, error) { + var s struct { + Projects []Project `json:"projects"` + } + err := (r.(ProjectPage)).ExtractInto(&s) + return s.Projects, err +} + +// Extract interprets any projectResults as a Project. +func (r projectResult) Extract() (*Project, error) { + var s struct { + Project *Project `json:"project"` + } + err := r.ExtractInto(&s) + return s.Project, err +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/urls.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/urls.go new file mode 100644 index 000000000..deeb24eaa --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects/urls.go @@ -0,0 +1,23 @@ +package projects + +import "github.com/opentelekomcloud/gophertelekomcloud" + +func listURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("projects") +} + +func getURL(client *golangsdk.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func createURL(client *golangsdk.ServiceClient) string { + return client.ServiceURL("projects") +} + +func deleteURL(client *golangsdk.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} + +func updateURL(client *golangsdk.ServiceClient, projectID string) string { + return client.ServiceURL("projects", projectID) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/doc.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/doc.go new file mode 100644 index 000000000..966e128f1 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/doc.go @@ -0,0 +1,108 @@ +/* +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(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(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + + authOptions = tokens.AuthOptions{ + UserID: "username", + Password: "password", + DomainName: "default", + } + + token, err = tokens.Create(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(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(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(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(identityClient, authOptions).ExtractToken() + if err != nil { + panic(err) + } + +*/ +package tokens diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/requests.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/requests.go new file mode 100644 index 000000000..d2a490c5f --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/requests.go @@ -0,0 +1,229 @@ +package tokens + +import ( + "github.com/opentelekomcloud/gophertelekomcloud" +) + +// 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 +} + +// 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]interface{}) (map[string]interface{}, error) + ToTokenV3ScopeMap() (map[string]interface{}, error) + CanReauth() bool + AuthTokenID() string + AuthHeaderDomainID() string +} + +// 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"` + + // 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:"-"` + + // Passcode is a Virtual MFA device verification code, which can be obtained on the MFA app. + Passcode string `json:"-"` + + Scope Scope `json:"-"` +} + +// ToTokenV3CreateMap builds a request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) { + golangsdkAuthOpts := golangsdk.AuthOptions{ + Username: opts.Username, + UserID: opts.UserID, + Password: opts.Password, + DomainID: opts.DomainID, + DomainName: opts.DomainName, + AllowReauth: opts.AllowReauth, + TokenID: opts.TokenID, + Passcode: opts.Passcode, + } + + return golangsdkAuthOpts.ToTokenV3CreateMap(scope) +} + +// ToTokenV3ScopeMap builds a scope request body from AuthOptions. +func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) { + if opts.Scope.ProjectName != "" { + // ProjectName provided: either DomainID or DomainName must also be supplied. + // ProjectID may not be supplied. + if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" { + return nil, golangsdk.ErrScopeDomainIDOrDomainName{} + } + if opts.Scope.ProjectID != "" { + return nil, golangsdk.ErrScopeProjectIDOrProjectName{} + } + + if opts.Scope.DomainID != "" { + // ProjectName + DomainID + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"id": &opts.Scope.DomainID}, + }, + }, nil + } + + if opts.Scope.DomainName != "" { + // ProjectName + DomainName + return map[string]interface{}{ + "project": map[string]interface{}{ + "name": &opts.Scope.ProjectName, + "domain": map[string]interface{}{"name": &opts.Scope.DomainName}, + }, + }, nil + } + } else if opts.Scope.ProjectID != "" { + // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided. + if opts.Scope.DomainID != "" { + return nil, golangsdk.ErrScopeProjectIDAlone{} + } + if opts.Scope.DomainName != "" { + return nil, golangsdk.ErrScopeProjectIDAlone{} + } + + // ProjectID + return map[string]interface{}{ + "project": map[string]interface{}{ + "id": &opts.Scope.ProjectID, + }, + }, nil + } else if opts.Scope.DomainID != "" { + // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided. + if opts.Scope.DomainName != "" { + return nil, golangsdk.ErrScopeDomainIDOrDomainName{} + } + + // DomainID + return map[string]interface{}{ + "domain": map[string]interface{}{ + "id": &opts.Scope.DomainID, + }, + }, nil + } else if opts.Scope.DomainName != "" { + // DomainName + return map[string]interface{}{ + "domain": map[string]interface{}{ + "name": &opts.Scope.DomainName, + }, + }, nil + } + + return nil, nil +} + +func (opts *AuthOptions) CanReauth() bool { + return opts.AllowReauth +} + +func (opts *AuthOptions) AuthTokenID() string { + return "" +} + +func (opts *AuthOptions) AuthHeaderDomainID() string { + return "" +} + +func subjectTokenHeaders(_ *golangsdk.ServiceClient, 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(c *golangsdk.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(tokenURL(c), b, &r.Body, &golangsdk.RequestOpts{ + MoreHeaders: map[string]string{ + "X-Auth-Token": opts.AuthTokenID(), + "X-Domain-Id": opts.AuthHeaderDomainID(), + }, + }) + r.Err = err + if resp != nil { + r.Header = resp.Header + } + return +} + +// Get validates and retrieves information about another token. +func Get(c *golangsdk.ServiceClient, token string) (r GetResult) { + resp, err := c.Get(tokenURL(c), &r.Body, &golangsdk.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + OkCodes: []int{200, 203}, + }) + if resp != nil { + r.Err = err + r.Header = resp.Header + } + return +} + +// Validate determines if a specified token is valid or not. +func Validate(c *golangsdk.ServiceClient, token string) (bool, error) { + resp, err := c.Request("HEAD", tokenURL(c), &golangsdk.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, 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(c *golangsdk.ServiceClient, token string) (r RevokeResult) { + _, r.Err = c.Delete(tokenURL(c), &golangsdk.RequestOpts{ + MoreHeaders: subjectTokenHeaders(c, token), + }) + return +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/results.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/results.go new file mode 100644 index 000000000..9e6ee7a46 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/results.go @@ -0,0 +1,179 @@ +package tokens + +import ( + "time" + + "github.com/opentelekomcloud/gophertelekomcloud" +) + +// 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"` + 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"` +} + +// 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 { + golangsdk.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 +} + +// 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 +} + +// 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 interface{}) error { + return r.ExtractIntoStructPtr(v, "token") +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/urls.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/urls.go new file mode 100644 index 000000000..62ff7287f --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens/urls.go @@ -0,0 +1,7 @@ +package tokens + +import "github.com/opentelekomcloud/gophertelekomcloud" + +func tokenURL(c *golangsdk.ServiceClient) string { + return c.ServiceURL("auth", "tokens") +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/loader.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/loader.go new file mode 100644 index 000000000..e2b11ac71 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/loader.go @@ -0,0 +1,697 @@ +package openstack + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" + + "gopkg.in/yaml.v2" + + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/openstack/utils" +) + +const ( + defaultEnvVarKey = "envvars" + defaultPrefix = "OS_" + + DefaultProfileName = "otc" + regionPlaceHolder = "{region_name}" +) + +var ( + yamlSuffixes = []string{".yaml", ".yml"} + jsonSuffixes = []string{".json"} + + configFiles = fileList("clouds") + secureFiles = fileList("secure") + vendorFiles = fileList("clouds-public") + + OTCVendorConfig = &VendorConfig{ + Clouds: map[string]Cloud{ + DefaultProfileName: { + AuthInfo: AuthInfo{ + AuthURL: fmt.Sprintf("https://iam.%s.otc.t-systems.com/v3", regionPlaceHolder), + }, + Regions: []string{"eu-de", "eu-nl"}, + EndpointType: "public", + IdentityAPIVersion: "3", + }, + }, + } +) + +func configSearchPath() []string { + home, _ := os.UserHomeDir() + cwd, _ := os.Getwd() + userConfigDir, _ := filepath.Abs(filepath.Join(home, ".config/openstack")) + unixConfigDir, _ := filepath.Abs("/etc/openstack") + return []string{ + cwd, + userConfigDir, + unixConfigDir, + } +} + +func fileList(name string) []string { + paths := configSearchPath() + var suffixes []string + suffixes = append(suffixes, yamlSuffixes...) + suffixes = append(suffixes, jsonSuffixes...) + size := len(suffixes) * len(paths) + files := make([]string, size) + i := 0 + for _, path := range paths { + for _, suffix := range suffixes { + files[i] = filepath.Join(path, name+suffix) + i++ + } + } + return files +} + +type Env struct { + // prefix of the invironment, `OS_` in most cases + prefix string + // cloud containins all information about used cloud + cloud *Cloud + // unstable make Env ignore lazy cloud loading and + // refresh it every time it's requested + unstable bool +} + +// NewEnv create new Env loader, lazy by default +func NewEnv(prefix string, lazy ...bool) *Env { + if prefix != "" && !strings.HasSuffix(prefix, "_") { + prefix += "_" + } + unstable := false + if len(lazy) > 0 { + unstable = !lazy[0] + } + return &Env{prefix: prefix, unstable: unstable} +} + +func (e *Env) Prefix() string { + return e.prefix +} + +func (e *Env) cloudFromEnv() *Cloud { + authOpts, _ := AuthOptionsFromEnv(e) + verify := true + if v := e.GetEnv("INSECURE"); v != "" { + verify = v != "1" && v != "true" + } + aws := NewEnv("AWS_") + access := aws.GetEnv("ACCESS_KEY_ID") + if access == "" { + access = e.GetEnv("ACCESS_KEY", "ACCESS_KEY_ID", "AK") + } + secret := aws.GetEnv("ACCESS_SECRET_KEY") + if secret == "" { + secret = e.GetEnv("SECRET_KEY", "ACCESS_KEY_SECRET", "SK") + } + security := aws.GetEnv("SECURITY_TOKEN") + if security == "" { + security = e.GetEnv("SECURITY_TOKEN", "AKSK_SECURITY_TOKEN", "ST") + } + region := e.GetEnv("REGION_NAME", "REGION_ID") + if region == "" { + region = utils.GetRegion(authOpts) + } + + cloud := &Cloud{ + Cloud: e.GetEnv("CLOUD"), + Profile: e.GetEnv("PROFILE"), + AuthInfo: AuthInfo{ + AuthURL: authOpts.IdentityEndpoint, + Token: authOpts.TokenID, + Username: authOpts.Username, + UserID: authOpts.UserID, + Password: authOpts.Password, + Passcode: authOpts.Passcode, + ProjectName: authOpts.TenantName, + ProjectID: authOpts.TenantID, + UserDomainName: e.GetEnv("USER_DOMAIN_NAME"), + UserDomainID: e.GetEnv("USER_DOMAIN_ID"), + ProjectDomainName: e.GetEnv("PROJECT_DOMAIN_NAME"), + ProjectDomainID: e.GetEnv("PROJECT_DOMAIN_ID"), + DomainName: authOpts.DomainName, + DomainID: authOpts.DomainID, + DefaultDomain: e.GetEnv("DEFAULT_DOMAIN"), + AccessKey: access, + SecretKey: secret, + SecurityToken: security, + AgencyName: authOpts.AgencyName, + AgencyDomainName: authOpts.AgencyDomainName, + DelegatedProject: authOpts.DelegatedProject, + }, + AuthType: AuthType(e.GetEnv("AUTH_TYPE")), + RegionName: region, + EndpointType: e.GetEnv("ENDPOINT_TYPE"), + Interface: e.GetEnv("INTERFACE"), + IdentityAPIVersion: e.GetEnv("IDENTITY_API_VERSION"), + VolumeAPIVersion: e.GetEnv("VOLUME_API_VERSION"), + Verify: &verify, + CACertFile: e.GetEnv("CA_CERT", "CA_CERT_FILE"), + ClientCertFile: e.GetEnv("CLIENT_CERT", "CLIENT_CERT_FILE"), + ClientKeyFile: e.GetEnv("CLIENT_KEY", "CLIENT_KEY_FILE"), + } + return cloud +} + +// GetEnv returns first non-empty value of given environment variables +func (e *Env) GetEnv(keys ...string) string { + for _, key := range keys { + if value := os.Getenv(e.prefix + key); value != "" { + return value + } + } + return "" +} + +// VendorConfig 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 VendorConfig struct { + Clouds map[string]Cloud `yaml:"public-clouds" json:"public-clouds"` +} + +// Config 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 Config struct { + DefaultCloud string `yaml:"-" json:"-"` + Clouds map[string]Cloud `yaml:"clouds" json:"clouds"` +} + +func NewConfig() *Config { + return &Config{ + Clouds: map[string]Cloud{}, + } +} + +// AuthType represents a valid method of authentication: `password`, `token`, `aksk` or `agency` +type AuthType string + +// AuthInfo represents the auth section of a cloud entry +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"` + + // Passcode for MFA. + Passcode string `yaml:"-" json:"-"` + + // 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 either 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"` + + // AK/SK auth means + AccessKey string `yaml:"ak,omitempty" json:"ak,omitempty"` + SecretKey string `yaml:"sk,omitempty" json:"sk,omitempty"` + SecurityToken string `yaml:"security_token,omitempty" json:"security_token,omitempty"` + + // OTC Agency config + AgencyName string `yaml:"target_agency_name,omitempty" json:"agency_name,omitempty"` + // AgencyDomainName is the name of domain who created the agency + AgencyDomainName string `yaml:"target_domain_id,omitempty" json:"target_domain_id,omitempty"` + // DelegatedProject is the name of delegated project + DelegatedProject string `yaml:"target_project_name,omitempty" json:"target_project_name,omitempty"` +} + +// 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"` + AuthType AuthType `yaml:"auth_type,omitempty" json:"auth_type,omitempty"` + AuthInfo AuthInfo `yaml:"auth,omitempty" json:"auth,omitempty"` + RegionName string `yaml:"region_name,omitempty" json:"region_name,omitempty"` + Regions []string `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"` +} + +func (c *Cloud) computeRegion() { + if c.RegionName != "" { + return + } + name := c.AuthInfo.ProjectName + if name == "" { + name = c.AuthInfo.DelegatedProject + } + c.RegionName = strings.Split(name, "_")[0] +} + +func (c *Cloud) computeAuthURL() error { + // Auth URL depends on provided region + if url := c.AuthInfo.AuthURL; strings.Contains(url, regionPlaceHolder) { + if c.RegionName == "" { + return fmt.Errorf("region placeholder found in `AuthURL` (%s), but no region provided", url) + } + c.AuthInfo.AuthURL = strings.ReplaceAll(url, regionPlaceHolder, c.RegionName) + } + return nil +} + +func loadFile(path string) ([]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer func() { _ = file.Close() }() + data, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + return data, nil +} + +func loadCloudFile(path string) (*Config, error) { + data, err := loadFile(path) + if err != nil { + return nil, err + } + clouds := NewConfig() + if err := yaml.Unmarshal(data, clouds); err != nil { + return nil, err + } + return clouds, err +} + +func loadVendorFile(path string) (*VendorConfig, error) { + data, err := loadFile(path) + if err != nil { + return nil, err + } + clouds := new(VendorConfig) + if err := yaml.Unmarshal(data, clouds); err != nil { + return nil, err + } + return clouds, err +} + +func mergeWithVendor(config *Config, vendor *VendorConfig) (*Config, error) { + for k, cloud := range config.Clouds { + profile := cloud.Profile + if profile == "" { + profile = cloud.Cloud + } + if profile == "" { + continue + } + if v, ok := vendor.Clouds[profile]; ok { + merged, err := mergeClouds(&cloud, &v) + if err != nil { + log.Printf("error during merge with vendor file: %s", err) + return config, err + } + config.Clouds[k] = *merged + } + } + return config, nil +} + +func mergeCloudConfigs(config, fallback *Config) (*Config, error) { + resultClouds := &Config{ + Clouds: map[string]Cloud{}, + } + for profile, cfg := range config.Clouds { + if fallback, ok := fallback.Clouds[profile]; ok { + cld, err := mergeClouds(cfg, fallback) + if err != nil { + return nil, err + } + resultClouds.Clouds[profile] = *cld + } else { + resultClouds.Clouds[profile] = cfg + } + } + return resultClouds, nil +} + +func selectExisting(files []string) string { + for _, file := range files { + if _, err := os.Stat(file); err == nil { + return file + } + } + return "" +} + +// mergeClouds merges two Config recursively (the AuthInfo also gets merged). +// In case both Config define a value, the value in the 'cloud' cloud takes precedence +func mergeClouds(cloud, fallback interface{}) (*Cloud, error) { + overrideJson, err := json.Marshal(fallback) + if err != nil { + return nil, err + } + cloudJson, err := json.Marshal(cloud) + if err != nil { + return nil, err + } + var fallbackInterface interface{} + err = json.Unmarshal(overrideJson, &fallbackInterface) + if err != nil { + return nil, err + } + var cloudInterface interface{} + err = json.Unmarshal(cloudJson, &cloudInterface) + if err != nil { + return nil, err + } + var mergedCloud Cloud + mergedInterface := utils.MergeInterfaces(cloudInterface, fallbackInterface) + mergedJson, err := json.Marshal(mergedInterface) + if err != nil { + return nil, err + } + if err := json.Unmarshal(mergedJson, &mergedCloud); err != nil { + return nil, err + } + return &mergedCloud, nil +} + +func mergeWithSecure(cloudConfig *Config, securePath string) *Config { + s, err := loadCloudFile(securePath) + if err != nil { + log.Printf("Failed to load %s as secure config", securePath) + return cloudConfig + } + cc, err := mergeCloudConfigs(cloudConfig, s) + if err != nil { + log.Printf("Failed to merge %s into cloud config", securePath) + return cloudConfig + } + return cc +} + +func mergeWithVendors(cloudConfig *Config, vendorPath string) *Config { + v, err := loadVendorFile(vendorPath) + if err != nil { + log.Printf("Failed to load %s as vendor config", vendorPath) + return cloudConfig + } + cc, err := mergeWithVendor(cloudConfig, v) + if err != nil { + log.Printf("Failed to merge %s into vendor config", vendorPath) + return cloudConfig + } + return cc +} + +// Cloud get cloud merged from configuration and env variables +// if `cloudName` is not empty, explicit cloud name will be used instead +// defined in `OS_CLOUD` environment variable +func (e *Env) Cloud(name ...string) (*Cloud, error) { + cloudName := "" + if len(name) > 0 { + cloudName = name[0] + e.unstable = true // previously loaded cloud can be different + } + if e.cloud == nil || e.unstable { + config, err := e.loadOpenstackConfig() + if err != nil { + return nil, fmt.Errorf("failed to load clouds configuration: %s", err) + } + if cloudName == "" { + cloudName = config.DefaultCloud + } + cloud, err := mergeClouds( + config.Clouds[cloudName], + e.cloudFromEnv(), + ) + if err != nil { + return nil, fmt.Errorf("failed to merge cloud %s with Env vars: %s", config.DefaultCloud, err) + } + cloud.Cloud = cloudName // override value read from environment + cloud.computeRegion() + if err := cloud.computeAuthURL(); err != nil { + return nil, err + } + e.cloud = cloud + } + return e.cloud, nil + +} + +// LoadCloudConfig utilize all existing cloud configurations to create cloud configuration: +// env variables, clouds.yaml, secure.yaml, clouds-public.yaml +func (e *Env) loadOpenstackConfig() (*Config, error) { + var ( + configs = make([]string, len(configFiles)) + secure = make([]string, len(secureFiles)) + vendors = make([]string, len(vendorFiles)) + ) + copy(configs, configFiles) + copy(secure, secureFiles) + copy(vendors, vendorFiles) + + // find config files + if c := e.GetEnv("CLIENT_CONFIG_FILE"); c != "" { + configs = utils.PrependString(c, configs) + } + configPath := selectExisting(configs) + + if s := e.GetEnv("CLIENT_SECURE_FILE"); s != "" { + secure = utils.PrependString(s, secure) + } + securePath := selectExisting(secure) + + if v := e.GetEnv("CLIENT_VENDOR_FILE"); v != "" { + vendors = utils.PrependString(v, vendors) + } + vendorPath := selectExisting(vendors) + + cloudConfig := NewConfig() + + // load clouds.yaml + if configPath != "" { + c, err := loadCloudFile(configPath) + if err != nil { + log.Printf("Failed to load %s as cloud config", securePath) + } + if c.Clouds != nil { + cloudConfig = c + } + } + + // merge with secure.yaml + if securePath != "" { + cloudConfig = mergeWithSecure(cloudConfig, securePath) + } + + // append cloud from envvars + envVarKey := e.GetEnv("CLOUD_NAME") + if envVarKey == "" { + envVarKey = defaultEnvVarKey + } + if _, ok := cloudConfig.Clouds[envVarKey]; ok { + return nil, fmt.Errorf("%sCLOUD_NAME=`%s` duplicates cloud defined in file", e.prefix, envVarKey) + } + cloudConfig.Clouds[envVarKey] = *NewEnv(envVarKey).cloudFromEnv() + + cloudName := e.GetEnv("CLOUD") + if cloudName == "" && len(cloudConfig.Clouds) == 1 { + for k := range cloudConfig.Clouds { + cloudName = k + } + } + cloudConfig.DefaultCloud = cloudName + + // merge with clouds-public.yaml + var err error + if vendorPath != "" { + cloudConfig = mergeWithVendors(cloudConfig, vendorPath) + } else { + cloudConfig, err = mergeWithVendor(cloudConfig, OTCVendorConfig) + } + return cloudConfig, err +} + +func getAuthType(val AuthType) AuthType { + explicitTypes := []string{"token", "password", "aksk"} + for _, opt := range explicitTypes { + if strings.Contains(string(val), opt) { + return AuthType(opt) + } + } + return val +} + +// AuthOptionsFromInfo builds auth options from auth info and type. Returns either AuthOptions or AKSKAuthOptions +func AuthOptionsFromInfo(authInfo *AuthInfo, authType AuthType) (golangsdk.AuthOptionsProvider, error) { + // project scope + if authInfo.ProjectID != "" || authInfo.ProjectName != "" { + if authInfo.ProjectDomainName != "" { + authInfo.DomainName = authInfo.ProjectDomainName + } + if authInfo.ProjectDomainID != "" { + authInfo.ProjectID = authInfo.ProjectDomainID + } + } + // user scope + if authInfo.Username != "" || authInfo.UserID != "" { + if authInfo.UserDomainName != "" { + authInfo.DomainName = authInfo.UserDomainName + } + if authInfo.UserDomainID != "" { + authInfo.ProjectID = authInfo.UserDomainID + } + } + + ao := golangsdk.AuthOptions{ + IdentityEndpoint: authInfo.AuthURL, + TokenID: authInfo.Token, + Username: authInfo.Username, + UserID: authInfo.UserID, + Password: authInfo.Password, + DomainID: authInfo.DomainID, + DomainName: authInfo.DomainName, + TenantID: authInfo.ProjectID, + TenantName: authInfo.ProjectName, + Passcode: authInfo.Passcode, + } + + explicitAuthType := getAuthType(authType) + + // 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 explicitAuthType == "token" || explicitAuthType == "aksk" { + ao.Username = "" + ao.Password = "" + ao.UserID = "" + ao.DomainID = "" + ao.DomainName = "" + } + + // Check for absolute minimum requirements. + if ao.IdentityEndpoint == "" { + err := golangsdk.ErrMissingInput{Argument: "auth_url"} + return nil, err + } + if explicitAuthType == "aksk" || (explicitAuthType == "" && authInfo.AccessKey != "") { + return golangsdk.AKSKAuthOptions{ + IdentityEndpoint: ao.IdentityEndpoint, + ProjectId: ao.TenantID, + ProjectName: ao.TenantName, + Domain: ao.DomainName, + DomainID: ao.DomainID, + AccessKey: authInfo.AccessKey, + SecretKey: authInfo.SecretKey, + AgencyName: ao.AgencyName, + AgencyDomainName: ao.AgencyDomainName, + DelegatedProject: ao.DelegatedProject, + }, nil + } + return ao, nil +} + +// AuthenticatedClient create new client based on used Env prefix +// this uses LoadOpenstackConfig inside +func (e *Env) AuthenticatedClient(cloudName ...string) (*golangsdk.ProviderClient, error) { + cloud, err := e.Cloud(cloudName...) + if err != nil { + return nil, err + } + return AuthenticatedClientFromCloud(cloud) +} + +// AuthenticatedClientFromCloud create new authenticated client for given cloud config +func AuthenticatedClientFromCloud(cloud *Cloud) (*golangsdk.ProviderClient, error) { + opts, err := AuthOptionsFromInfo(&cloud.AuthInfo, cloud.AuthType) + if err != nil { + return nil, fmt.Errorf("failed to convert AuthInfo to AuthOptsBuilder with Env vars: %s", err) + } + client, err := AuthenticatedClient(opts) + if err != nil { + return nil, fmt.Errorf("failed to authenticate client: %s", err) + } + return client, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/base_endpoint.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/base_endpoint.go new file mode 100644 index 000000000..40080f7af --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/base_endpoint.go @@ -0,0 +1,28 @@ +package utils + +import ( + "net/url" + "regexp" + "strings" +) + +// BaseEndpoint will return a URL without the /vX.Y +// portion of the URL. +func BaseEndpoint(endpoint string) (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) + u.Path = path[:versionIndex] + } + + return u.String(), nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/choose_version.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/choose_version.go new file mode 100644 index 000000000..05f85a179 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/choose_version.go @@ -0,0 +1,111 @@ +package utils + +import ( + "fmt" + "strings" + + "github.com/opentelekomcloud/gophertelekomcloud" +) + +// 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 most recent non-experimental alternative from a service's +// published versions. +// It returns the highest-Priority Version among the alternatives that are provided, as well as its corresponding endpoint. +func ChooseVersion(client *golangsdk.ProviderClient, recognized []*Version) (*Version, string, error) { + 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("GET", client.IdentityBase, &golangsdk.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/opentelekomcloud/gophertelekomcloud/openstack/utils/utils.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/utils.go new file mode 100644 index 000000000..c7befc5fd --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/openstack/utils/utils.go @@ -0,0 +1,102 @@ +package utils + +import ( + "os" + "reflect" + "strings" + + golangsdk "github.com/opentelekomcloud/gophertelekomcloud" +) + +func DeleteNotPassParams(params *map[string]interface{}, notPassParams []string) { + for _, i := range notPassParams { + delete(*params, i) + } +} + +// 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 + } + overriding = append(overriding, list...) + 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 + } +} + +func PrependString(item string, slice []string) []string { + result := make([]string, len(slice)+1) + result[0] = item + for i, v := range slice { + result[i+1] = v + } + return result +} + +func In(item interface{}, slice interface{}) bool { + for _, it := range slice.([]interface{}) { + if reflect.DeepEqual(item, it) { + return true + } + } + return false +} + +// GetRegion returns the region that was specified in the auth options. If a +// region was not set it returns value from env OS_REGION_NAME +func GetRegion(authOpts golangsdk.AuthOptions) string { + name := authOpts.TenantName + if name == "" { + name = authOpts.DelegatedProject + } + region := strings.Split(name, "_")[0] + return getenv("OS_REGION_NAME", region) +} + +// GetRegionFromAKSK returns the region that was specified in the auth options. +// If a region was not set it returns value from env OS_REGION_NAME +func GetRegionFromAKSK(authOpts golangsdk.AKSKAuthOptions) string { + name := authOpts.ProjectName + if name == "" { + name = authOpts.DelegatedProject + } + region := strings.Split(name, "_")[0] + return getenv("OS_REGION_NAME", region) +} + +// getenv returns value from env is present or default value +func getenv(key, fallback string) string { + value := os.Getenv(key) + if value == "" { + return fallback + } + return value +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/http.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/http.go new file mode 100644 index 000000000..a51b910e7 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/http.go @@ -0,0 +1,106 @@ +package pagination + +import ( + "bytes" + "io" + "net/http" + "net/url" + + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +// PageResult stores the HTTP response that returned the current page of results. +type PageResult struct { + golangsdk.Result + url.URL +} + +func (r PageResult) GetBody() []byte { + return r.Body +} + +// GetBodyAsSlice tries to convert page body to a slice, returning nil on fail +func (r PageResult) GetBodyAsSlice() ([]any, error) { + result := make([]any, 0) + + if err := extract.Into(bytes.NewReader(r.Body), &result); err != nil { + return nil, err + } + + return result, nil +} + +// GetBodyAsMap tries to convert page body to a map, returning nil on fail +func (r PageResult) GetBodyAsMap() (map[string]any, error) { + result := make(map[string]any, 0) + + if err := extract.Into(bytes.NewReader(r.Body), &result); err != nil { + return nil, err + } + + return result, nil +} + +// 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) { + defer resp.Body.Close() + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return PageResult{}, err + } + + return PageResult{ + Result: golangsdk.Result{ + Body: rawBody, + Header: resp.Header, + }, + URL: *resp.Request.URL, + }, nil +} + +// Request performs an HTTP request and extracts the http.Response from the result. +func Request(client *golangsdk.ServiceClient, headers map[string]string, url string) (*http.Response, error) { + return client.Get(url, nil, &golangsdk.RequestOpts{ + MoreHeaders: headers, + OkCodes: []int{200, 204, 300}, + }) +} + +// NewPageResult stores the HTTP response that returned the current page of results. +type NewPageResult struct { + // Body is the payload of the HTTP response from the server. + Body []byte + + // Header contains the HTTP header structure from the original response. + Header http.Header + + URL url.URL +} + +func (r NewPageResult) NewGetBody() []byte { + return r.Body +} + +// NewGetBodyAsMap tries to convert page body to a map, returning nil on fail +func (r NewPageResult) NewGetBodyAsMap() (map[string]any, error) { + result := make(map[string]any, 0) + + if err := extract.Into(bytes.NewReader(r.Body), &result); err != nil { + return nil, err + } + + return result, nil +} + +// NewGetBodyAsSlice tries to convert page body to a slice, returning nil on fail +func (r NewPageResult) NewGetBodyAsSlice() ([]any, error) { + result := make([]any, 0) + + if err := extract.Into(bytes.NewReader(r.Body), &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/info.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/info.go new file mode 100644 index 000000000..0b7644e48 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/info.go @@ -0,0 +1,54 @@ +package pagination + +import ( + "bytes" + + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +// PageWithInfo is a page with marker information inside `page_info` +type PageWithInfo struct { + MarkerPageBase +} + +type pageInfo struct { + PreviousMarker string `json:"previous_marker"` + NextMarker string `json:"next_marker"` + CurrentCount int `json:"current_count"` +} + +func (p PageWithInfo) LastMarker() (string, error) { + var info pageInfo + err := extract.IntoStructPtr(bytes.NewReader(p.Body), &info, "page_info") + if err != nil { + return "", err + } + return info.NextMarker, nil +} + +// NextPageURL generates the URL for the page of results after this one. +func (p PageWithInfo) NextPageURL() (string, error) { + currentURL := p.URL + + mark, err := p.Owner.LastMarker() + if err != nil { + return "", err + } + if mark == "" { + return "", nil + } + + q := currentURL.Query() + q.Set("marker", mark) + currentURL.RawQuery = q.Encode() + + return currentURL.String(), nil +} + +func NewPageWithInfo(r PageResult) PageWithInfo { + p := PageWithInfo{MarkerPageBase: MarkerPageBase{ + PageResult: r, + }} + p.Owner = &p + return p +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/linked.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/linked.go new file mode 100644 index 000000000..3b374923d --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/linked.go @@ -0,0 +1,110 @@ +package pagination + +import ( + "bytes" + "fmt" + "reflect" + + "github.com/opentelekomcloud/gophertelekomcloud" + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +// 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 := make(map[string]any) + + err := extract.Into(bytes.NewReader(current.Body), &submap) + if err != nil { + err := golangsdk.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 := golangsdk.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 := golangsdk.ErrUnexpectedType{} + err.Expected = "string" + err.Actual = fmt.Sprintf("%v", reflect.TypeOf(value)) + return "", err + } + + return url, nil + } + } +} + +// IsEmpty satisfies the IsEmpty method of the Page interface +func (current LinkedPageBase) IsEmpty() (bool, error) { + body, err := current.GetBodyAsSlice() + if err != nil { + return false, fmt.Errorf("error converting page body to slice: %w", err) + } + + return len(body) == 0, nil +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current LinkedPageBase) GetBody() []byte { + return current.Body +} + +// WrapNextPageURL function use makerID to warp next page url,it returns the full url for request. +func (current LinkedPageBase) WrapNextPageURL(markerID string) (string, error) { + limit := current.URL.Query().Get("limit") + + if limit == "" { + return "", nil + } + + q := current.URL.Query() + + q.Set("marker", markerID) + current.URL.RawQuery = q.Encode() + return current.URL.String(), nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/marker.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/marker.go new file mode 100644 index 000000000..a2cd397e8 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/marker.go @@ -0,0 +1,54 @@ +package pagination + +import ( + "fmt" +) + +// 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) { + body, err := current.GetBodyAsSlice() + if err != nil { + return false, fmt.Errorf("error converting page body to slice: %w", err) + } + + return len(body) == 0, nil +} + +// GetBody returns the linked page's body. This method is needed to satisfy the +// Page interface. +func (current MarkerPageBase) GetBody() []byte { + return current.Body +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/offset.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/offset.go new file mode 100644 index 000000000..b337306f9 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/offset.go @@ -0,0 +1,60 @@ +package pagination + +import ( + "fmt" + "strconv" +) + +type OffsetPage interface { + // LastElement returning index of the last element of the page + LastElement() int +} + +type OffsetPageBase struct { + Offset int + Limit int + + PageResult +} + +func (p OffsetPageBase) LastElement() int { + q := p.URL.Query() + offset, err := strconv.Atoi(q.Get("offset")) + if err != nil { + offset = p.Offset + q.Set("offset", strconv.Itoa(offset)) + } + limit, err := strconv.Atoi(q.Get("limit")) + if err != nil { + limit = p.Limit + q.Set("limit", strconv.Itoa(limit)) + } + return offset + limit +} + +func (p OffsetPageBase) NextPageURL() (string, error) { + currentURL := p.URL + q := currentURL.Query() + if q.Get("offset") == "" && q.Get("limit") == "" { + // without offset and limit it's just a SinglePageBase + return "", nil + } + q.Set("offset", strconv.Itoa(p.LastElement())) + currentURL.RawQuery = q.Encode() + return currentURL.String(), nil +} + +// IsEmpty returns true if this Page has no items in it. +func (p OffsetPageBase) IsEmpty() (bool, error) { + body, err := p.GetBodyAsSlice() + if err != nil { + return false, fmt.Errorf("error converting page body to slice: %w", err) + } + + return len(body) == 0, nil +} + +// GetBody returns the Page Body. This is used in the `AllPages` method. +func (p OffsetPageBase) GetBody() []byte { + return p.Body +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/pager.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/pager.go new file mode 100644 index 000000000..5a3f9c6eb --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/pager.go @@ -0,0 +1,440 @@ +package pagination + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "reflect" + "strings" + + "github.com/opentelekomcloud/gophertelekomcloud" +) + +// 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() []byte + // GetBodyAsSlice tries to convert page body to a slice. + GetBodyAsSlice() ([]any, error) + // GetBodyAsMap tries to convert page body to a map. + GetBodyAsMap() (map[string]any, error) +} + +// Pager knows how to advance through a specific resource collection, one page at a time. +type Pager struct { + client *golangsdk.ServiceClient + Client *golangsdk.ServiceClient + + initialURL string + InitialURL string + + createPage func(r PageResult) Page + CreatePage func(r NewPageResult) NewPage + + 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 *golangsdk.ServiceClient, initialURL string, createPage func(r PageResult) Page) Pager { + return Pager{ + client: client, + initialURL: initialURL, + createPage: createPage, + } +} + +func (p Pager) fetchNextPage(url string) (Page, error) { + resp, err := Request(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(handler func(Page) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.initialURL + for { + currentPage, err := p.fetchNextPage(currentURL) + if err != nil { + return err + } + + empty, err := currentPage.IsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(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() (Page, error) { + // body will contain the final concatenated Page body. + var body []byte + + // Grab a test page to ascertain the page body type. + testPage, err := p.fetchNextPage(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(testPage) + + // if it's a single page, just return the testPage (first page) + if _, found := pageType.FieldByName("SinglePageBase"); found { + return testPage, nil + } + + if _, err := testPage.GetBodyAsSlice(); err == nil { + var pagesSlice []any + + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b, err := page.GetBodyAsSlice() + if err != nil { + return false, fmt.Errorf("error paginating page with slice body: %w", err) + } + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + + body, err = json.Marshal(pagesSlice) + if err != nil { + return nil, err + } + } else if _, err := testPage.GetBodyAsMap(); err == nil { + var pagesSlice []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(func(page Page) (bool, error) { + b, err := page.GetBodyAsMap() + if err != nil { + return false, fmt.Errorf("error paginating page with map body: %w", err) + } + 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]interface{}) + switch vt := v.(type) { + case []any: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + return true, nil + }) + if err != nil { + return nil, err + } + + mapBody := map[string]any{ + key: pagesSlice, + } + + body, err = json.Marshal(mapBody) + if err != nil { + return nil, err + } + } else { + var pagesSlice [][]byte + + // Iterate over the pages to concatenate the bodies. + err = p.EachPage(func(page Page) (bool, error) { + b := page.GetBody() + pagesSlice = append(pagesSlice, b) + // separate 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...) + } + + body = b + } + + // 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(reflect.ValueOf(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 +} + +// NewPage 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 NewPage interface { + // NewNextPageURL generates the URL for the page of data that follows this collection. + // Return "" if no such page exists. + NewNextPageURL() (string, error) + + // NewIsEmpty returns true if this Page has no items in it. + NewIsEmpty() (bool, error) + + // NewGetBody returns the Page Body. This is used in the `AllPages` method. + NewGetBody() []byte + // NewGetBodyAsSlice tries to convert page body to a slice. + NewGetBodyAsSlice() ([]any, error) + // NewGetBodyAsMap tries to convert page body to a map. + NewGetBodyAsMap() (map[string]any, error) +} + +func (p Pager) newFetchNextPage(url string) (NewPage, error) { + resp, err := Request(p.Client, p.Headers, url) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + rawBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return p.CreatePage(NewPageResult{ + Body: rawBody, + Header: resp.Header, + URL: *resp.Request.URL, + }), nil +} + +// NewEachPage 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) NewEachPage(handler func(NewPage) (bool, error)) error { + if p.Err != nil { + return p.Err + } + currentURL := p.InitialURL + for { + currentPage, err := p.newFetchNextPage(currentURL) + if err != nil { + return err + } + + empty, err := currentPage.NewIsEmpty() + if err != nil { + return err + } + if empty { + return nil + } + + ok, err := handler(currentPage) + if err != nil { + return err + } + if !ok { + return nil + } + + currentURL, err = currentPage.NewNextPageURL() + if err != nil { + return err + } + if currentURL == "" { + return nil + } + } +} + +// NewAllPages 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) NewAllPages() (NewPage, error) { + // body will contain the final concatenated Page body. + var body []byte + + // Grab a test page to ascertain the page body type. + testPage, err := p.newFetchNextPage(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(testPage) + + // if it's a single page, just return the testPage (first page) + if _, found := pageType.FieldByName("NewSinglePageBase"); found { + return testPage, nil + } + + if _, err := testPage.NewGetBodyAsSlice(); err == nil { + var pagesSlice []any + + // Iterate over the pages to concatenate the bodies. + err = p.NewEachPage(func(page NewPage) (bool, error) { + b, err := page.NewGetBodyAsSlice() + if err != nil { + return false, fmt.Errorf("error paginating page with slice body: %w", err) + } + pagesSlice = append(pagesSlice, b...) + return true, nil + }) + if err != nil { + return nil, err + } + + body, err = json.Marshal(pagesSlice) + if err != nil { + return nil, err + } + } else if _, err := testPage.NewGetBodyAsMap(); err == nil { + var pagesSlice []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.NewEachPage(func(page NewPage) (bool, error) { + b, err := page.NewGetBodyAsMap() + if err != nil { + return false, fmt.Errorf("error paginating page with map body: %w", err) + } + 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]interface{}) + switch vt := v.(type) { + case []any: + key = k + pagesSlice = append(pagesSlice, vt...) + } + } + } + return true, nil + }) + if err != nil { + return nil, err + } + + mapBody := map[string]any{ + key: pagesSlice, + } + + body, err = json.Marshal(mapBody) + if err != nil { + return nil, err + } + } else { + var pagesSlice [][]byte + + // Iterate over the pages to concatenate the bodies. + err = p.NewEachPage(func(page NewPage) (bool, error) { + b := page.NewGetBody() + pagesSlice = append(pagesSlice, b) + // separate 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...) + } + + body = b + } + + // 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(reflect.ValueOf(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().(NewPage), err +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/pkg.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/pkg.go new file mode 100644 index 000000000..912daea36 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/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/opentelekomcloud/gophertelekomcloud/pagination/single.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/single.go new file mode 100644 index 000000000..622b391bb --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/pagination/single.go @@ -0,0 +1,56 @@ +package pagination + +import ( + "fmt" +) + +// SinglePageBase may be embedded in a Page that contains all the results from an operation at once. +// Deprecated: use element slice as a return result. +type SinglePageBase PageResult + +func (current SinglePageBase) GetBody() []byte { + return current.Body +} + +func (current SinglePageBase) GetBodyAsSlice() ([]interface{}, error) { + return PageResult(current).GetBodyAsSlice() +} + +func (current SinglePageBase) GetBodyAsMap() (map[string]interface{}, error) { + return PageResult(current).GetBodyAsMap() +} + +// NextPageURL always returns "" to indicate that there are no more pages to return. +func (current SinglePageBase) NextPageURL() (string, error) { + return "", nil +} + +// IsEmpty satisfies the IsEmpty method of the Page interface +func (current SinglePageBase) IsEmpty() (bool, error) { + body, err := current.GetBodyAsSlice() + if err != nil { + return false, fmt.Errorf("error converting page body to slice: %w", err) + } + + return len(body) == 0, nil +} + +// NewSinglePageBase may be embedded in a Page that contains all the results from an operation at once. +type NewSinglePageBase struct { + NewPageResult +} + +// NewNextPageURL always returns "" to indicate that there are no more pages to return. +func (current NewSinglePageBase) NewNextPageURL() (string, error) { + return "", nil +} + +// NewIsEmpty satisfies the IsEmpty method of the Page interface +func (current NewSinglePageBase) NewIsEmpty() (bool, error) { + body, err := current.NewGetBodyAsSlice() + if err != nil { + return false, fmt.Errorf("error converting page body to slice: %w", err) + } + + return len(body) == 0, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/params.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/params.go new file mode 100644 index 000000000..27fdaa223 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/params.go @@ -0,0 +1,523 @@ +package golangsdk + +import ( + "encoding/json" + "fmt" + "net/url" + "reflect" + "strconv" + "strings" + "time" +) + +/* +BuildRequestBody builds a map[string]interface from the given `struct`. If +parent is not an empty string, the final map[string]interface returned will +encapsulate the built one. + +Deprecated: use `build.RequestBody` instead. + +For example: + + disk := 1 + createOpts := flavors.CreateOpts{ + ID: "1", + Name: "m1.tiny", + Disk: &disk, + RAM: 512, + VCPUs: 1, + RxTxFactor: 1.0, + } + + body, err := golangsdk.BuildRequestBody(createOpts, "flavor") + +The above example can be run as-is, however it is recommended to look at how +BuildRequestBody is used within Gophercloud to more fully understand how it +fits within the request process as a whole rather than use it directly as shown +above. +*/ +func BuildRequestBody(opts interface{}, parent string) (map[string]interface{}, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]interface{}) + if optsValue.Kind() == reflect.Struct { + // fmt.Printf("optsValue.Kind() is a reflect.Struct: %+v\n", optsValue.Kind()) + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + + // nolint + if f.Name != strings.Title(f.Name) { + // fmt.Printf("Skipping field: %s...\n", f.Name) + continue + } + + // fmt.Printf("Starting on field: %s...\n", f.Name) + + zero := isZero(v) + // fmt.Printf("v is zero?: %v\n", zero) + + // 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 + err := ErrMissingInput{} + err.Argument = f.Name + return nil, err + } + } + + if xorTag := f.Tag.Get("xor"); xorTag != "" { + // fmt.Printf("Checking `xor` tag for field [%s] with value %+v:\n\txorTag: %s\n", f.Name, v, xorTag) + xorField := optsValue.FieldByName(xorTag) + var xorFieldIsZero bool + if reflect.ValueOf(xorField.Interface()) == reflect.Zero(xorField.Type()) { + xorFieldIsZero = true + } else { + if xorField.Kind() == reflect.Ptr { + xorField = xorField.Elem() + } + xorFieldIsZero = isZero(xorField) + } + if !(zero != xorFieldIsZero) { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, xorTag) + err.Info = fmt.Sprintf("Exactly one of %s and %s must be provided", f.Name, xorTag) + return nil, err + } + } + + if orTag := f.Tag.Get("or"); orTag != "" { + // fmt.Printf("Checking `or` tag for field with:\n\tname: %+v\n\torTag:%s\n", f.Name, orTag) + // fmt.Printf("field is zero?: %v\n", zero) + if zero { + orField := optsValue.FieldByName(orTag) + var orFieldIsZero bool + if reflect.ValueOf(orField.Interface()) == reflect.Zero(orField.Type()) { + orFieldIsZero = true + } else { + if orField.Kind() == reflect.Ptr { + orField = orField.Elem() + } + orFieldIsZero = isZero(orField) + } + if orFieldIsZero { + err := ErrMissingInput{} + err.Argument = fmt.Sprintf("%s/%s", f.Name, orTag) + err.Info = fmt.Sprintf("At least one of %s and %s must be provided", f.Name, orTag) + return nil, err + } + } + } + + if v.Kind() == reflect.Struct || (v.Kind() == reflect.Ptr && v.Elem().Kind() == reflect.Struct) { + if zero { + // fmt.Printf("value before change: %+v\n", optsValue.Field(i)) + if jsonTag := f.Tag.Get("json"); jsonTag != "" { + jsonTagPieces := strings.Split(jsonTag, ",") + if len(jsonTagPieces) > 1 && jsonTagPieces[1] == "omitempty" { + if v.CanSet() { + if !v.IsNil() { + if v.Kind() == reflect.Ptr { + v.Set(reflect.Zero(v.Type())) + } + } + // fmt.Printf("value after change: %+v\n", optsValue.Field(i)) + } + } + } + continue + } + + // fmt.Printf("Calling BuildRequestBody with:\n\tv: %+v\n\tf.Name:%s\n", v.Interface(), f.Name) + _, err := BuildRequestBody(v.Interface(), f.Name) + if err != nil { + return nil, err + } + } + } + + // fmt.Printf("opts: %+v \n", opts) + + b, err := json.Marshal(opts) + if err != nil { + return nil, err + } + + // fmt.Printf("string(b): %s\n", string(b)) + + err = json.Unmarshal(b, &optsMap) + if err != nil { + return nil, err + } + + // fmt.Printf("optsMap: %+v\n", optsMap) + + if parent != "" { + optsMap = map[string]interface{}{parent: optsMap} + } + // fmt.Printf("optsMap after parent added: %+v\n", optsMap) + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("options type is not a struct") +} + +// EnabledState is a convenience type, mostly used in Create and Update +// operations. Because the zero value of a bool is FALSE, we need to use a +// pointer instead to indicate zero-ness. +// Deprecated, use pointerto.Bool instead +type EnabledState *bool + +// Convenience vars for EnabledState values. +// Deprecated: use `pointerto.Bool` instead. +var ( + iTrue = true + iFalse = false + + // Enabled is a pointer to `true`. + Enabled EnabledState = &iTrue + // Disabled is a pointer to `false`. + Disabled EnabledState = &iFalse +) + +// IPVersion is a type for the possible IP address versions. Valid instances +// are IPv4 and IPv6 +type IPVersion int + +const ( + // IPv4 is used for IP version 4 addresses + IPv4 IPVersion = 4 + // IPv6 is used for IP version 6 addresses + IPv6 IPVersion = 6 +) + +// IntToPointer is a function for converting integers into integer pointers. +// This is useful when passing in options to operations. +// Deprecated: use `pointerto.Int` instead. +func IntToPointer(i int) *int { + return &i +} + +/* +MaybeString is an internal function to be used by request methods in individual +resource packages. + +It takes a string that might be a zero value and returns either a pointer to its +address or nil. This is useful for allowing users to conveniently omit values +from an options struct by leaving them zeroed, but still pass nil to the JSON +serializer so they'll be omitted from the request body. + +Deprecated +*/ +func MaybeString(original string) *string { + if original != "" { + return &original + } + return nil +} + +/* +MaybeInt is an internal function to be used by request methods in individual +resource packages. + +Like MaybeString, it accepts an int that may or may not be a zero value, and +returns either a pointer to its address or nil. It's intended to hint that the +JSON serializer should omit its field. + +Deprecated +*/ +func MaybeInt(original int) *int { + if original != 0 { + return &original + } + return nil +} + +var t time.Time + +// isZero checks if given argument has default type value. +func isZero(v reflect.Value) bool { + // fmt.Printf("\n\nchecking isZero for value: %+v\n", v) + switch v.Kind() { + case reflect.Ptr: + if v.IsNil() { + return true + } + return false + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + z := true + for i := 0; i < v.Len(); i++ { + z = z && isZero(v.Index(i)) + } + return z + case reflect.Struct: + if v.Type() == reflect.TypeOf(t) { + return v.Interface().(time.Time).IsZero() + } + z := true + for i := 0; i < v.NumField(); i++ { + z = z && isZero(v.Field(i)) + } + return z + } + // Compare other types directly: + z := reflect.Zero(v.Type()) + // fmt.Printf("zero type for value: %+v\n\n\n", z) + return v.Interface() == z.Interface() +} + +/* +BuildQueryString is an internal function to be used by request methods in +individual resource packages. + +It accepts a tagged structure and expands it into a URL struct. Field names are +converted into query parameters based on a "q" tag. For example: + + type struct Something { + Bar string `q:"x_bar"` + Baz int `q:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into "?x_bar=AAA&lorem_ipsum=BBB". + +The struct's fields may be strings, integers, or boolean values. Fields left at +their type's zero value will be omitted from the query. +*/ +func BuildQueryString(opts interface{}) (*url.URL, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + params := url.Values{} + + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + qTag := f.Tag.Get("q") + + // if the field has a 'q' tag, it goes in the query string + if qTag != "" { + tags := strings.Split(qTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + loop: + switch v.Kind() { + case reflect.Ptr: + v = v.Elem() + goto loop + case reflect.String: + params.Add(tags[0], v.String()) + case reflect.Int: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Int64: + params.Add(tags[0], strconv.FormatInt(v.Int(), 10)) + case reflect.Bool: + params.Add(tags[0], strconv.FormatBool(v.Bool())) + case reflect.Slice: + switch v.Type().Elem() { + case reflect.TypeOf(0): + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], strconv.FormatInt(v.Index(i).Int(), 10)) + } + default: + for i := 0; i < v.Len(); i++ { + params.Add(tags[0], v.Index(i).String()) + } + } + case reflect.Map: + if v.Type().Key().Kind() == reflect.String && v.Type().Elem().Kind() == reflect.String { + var s []string + for _, k := range v.MapKeys() { + value := v.MapIndex(k).String() + s = append(s, fmt.Sprintf("'%s':'%s'", k.String(), value)) + } + params.Add(tags[0], fmt.Sprintf("{%s}", strings.Join(s, ", "))) + } + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return &url.URL{}, fmt.Errorf("required query parameter [%s] not set", f.Name) + } + } + } + } + + return &url.URL{RawQuery: params.Encode()}, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return nil, fmt.Errorf("options type is not a struct") +} + +// URL is a representation for the url.URL from the standard library +type URL struct { + u *url.URL +} + +func (u *URL) String() string { + return u.u.String() +} + +// URLBuilder provides ability to build custom url with query parameters. +type URLBuilder struct { + endpoints []string + params interface{} +} + +// WithEndpoints accept strings and build url path +// Example: WithEndpoints("foo", "bar") will be modified to url with path "foo/bar" +// Characters /!?$#=&+ are not allowed in the endpoints. +func (ub *URLBuilder) WithEndpoints(endpoints ...string) *URLBuilder { + ub.endpoints = endpoints + return ub +} + +// WithQueryParams accept a struct and build query parameters for url. +// Example: +// +// type exampleStruct struct { +// TaskID string `q:"task_id"` +// } +// +// WithQueryParams(&exampleStruct{TaskID: "12345"}) will be modified to query parameters "?task_id=12345" +func (ub *URLBuilder) WithQueryParams(params interface{}) *URLBuilder { + ub.params = params + return ub +} + +// Build constructs and return url. +func (ub *URLBuilder) Build() (*URL, error) { + var u url.URL + + if ub.params != nil { + u1, err := BuildQueryString(ub.params) + + if err != nil { + return nil, err + } + + u = *u1 + } + + for _, e := range ub.endpoints { + if strings.ContainsAny(e, "/!?$#=&+") { + return nil, fmt.Errorf("characters '/!?$#=&+_\"' are not possible in endpoints") + } + } + + u.Path = strings.Join(ub.endpoints, "/") + + if _, err := url.Parse(u.String()); err != nil { + return nil, err + } + + return &URL{u: &u}, nil +} + +// NewURLBuilder is the constructor for a struct URLBuilder +func NewURLBuilder() *URLBuilder { + return &URLBuilder{} +} + +/* +BuildHeaders is an internal function to be used by request methods in +individual resource packages. + +It accepts an arbitrary tagged structure and produces a string map that's +suitable for use as the HTTP headers of an outgoing request. Field names are +mapped to header names based in "h" tags. + + type struct Something { + Bar string `h:"x_bar"` + Baz int `h:"lorem_ipsum"` + } + + instance := Something{ + Bar: "AAA", + Baz: "BBB", + } + +will be converted into: + + map[string]string{ + "x_bar": "AAA", + "lorem_ipsum": "BBB", + } + +Untagged fields and fields left at their zero values are skipped. Integers, +booleans and string values are supported. +*/ +func BuildHeaders(opts interface{}) (map[string]string, error) { + optsValue := reflect.ValueOf(opts) + if optsValue.Kind() == reflect.Ptr { + optsValue = optsValue.Elem() + } + + optsType := reflect.TypeOf(opts) + if optsType.Kind() == reflect.Ptr { + optsType = optsType.Elem() + } + + optsMap := make(map[string]string) + if optsValue.Kind() == reflect.Struct { + for i := 0; i < optsValue.NumField(); i++ { + v := optsValue.Field(i) + f := optsType.Field(i) + hTag := f.Tag.Get("h") + + // if the field has a 'h' tag, it goes in the header + if hTag != "" { + tags := strings.Split(hTag, ",") + + // if the field is set, add it to the slice of query pieces + if !isZero(v) { + switch v.Kind() { + case reflect.String: + optsMap[tags[0]] = v.String() + case reflect.Int: + optsMap[tags[0]] = strconv.FormatInt(v.Int(), 10) + case reflect.Bool: + optsMap[tags[0]] = strconv.FormatBool(v.Bool()) + } + } else { + // Otherwise, the field is not set. + if len(tags) == 2 && tags[1] == "required" { + // And the field is required. Return an error. + return optsMap, fmt.Errorf("Required header not set.") + } + } + } + + } + return optsMap, nil + } + // Return an error if the underlying type of 'opts' isn't a struct. + return optsMap, fmt.Errorf("options type is not a struct") +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/provider_client.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/provider_client.go new file mode 100644 index 000000000..b5a9a2779 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/provider_client.go @@ -0,0 +1,464 @@ +package golangsdk + +import ( + "bytes" + "fmt" + "io" + "net/http" + "strings" + "sync" + "time" + + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +// DefaultUserAgent is the default User-Agent string set in the request header. +const DefaultUserAgent = "golangsdk/2.0.0" + +// UserAgent represents a User-Agent header. +type UserAgent struct { + // prepend is the slice of User-Agent strings to prepend to DefaultUserAgent. + // All the strings to prepend are accumulated and prepended in the Join method. + prepend []string +} + +// Prepend prepends a user-defined string to the default User-Agent string. Users +// may pass in one or more strings to prepend. +func (ua *UserAgent) Prepend(s ...string) { + ua.prepend = append(s, ua.prepend...) +} + +// Join concatenates all the user-defined User-Agent strings with the default +// Gophercloud User-Agent string. +func (ua *UserAgent) Join() string { + uaSlice := append(ua.prepend, DefaultUserAgent) + return strings.Join(uaSlice, " ") +} + +// ProviderClient stores details that are required to interact with any +// services within a specific provider's API. +// +// Generally, you acquire a ProviderClient by calling the NewClient method in +// the appropriate provider's child package, providing whatever authentication +// credentials are required. +type ProviderClient struct { + // IdentityBase is the base URL used for a particular provider's identity + // service - it will be used when issuing authentication requests. It + // should point to the root resource of the identity service, not a specific + // identity version. + IdentityBase string + + // IdentityEndpoint is the identity endpoint. This may be a specific version + // of the identity service. If this is the case, this endpoint is used rather + // than querying versions first. + IdentityEndpoint string + + // TokenID is the ID of the most recently issued valid token. + // NOTE: Aside from within a custom ReauthFunc, this field shouldn't be set by an application. + // To safely read or write this value, call `Token` or `SetToken`, respectively + TokenID string + + // ProjectID is the ID of project to which User is authorized. + ProjectID string + + // UserID is the ID of the authorized user + UserID string + + // DomainID is the ID of project to which User is authorized. + DomainID string + + // RegionID is the Name of region to which User is authorized. + RegionID string + + // EndpointLocator describes how this provider discovers the endpoints for + // its constituent services. + EndpointLocator EndpointLocator + + // HTTPClient allows users to interject arbitrary http, https, or other transit behaviors. + HTTPClient http.Client + + // UserAgent represents the User-Agent header in the HTTP request. + UserAgent UserAgent + + // MaxBackoffRetries set the maximum number of backoffs. When not set, defaults to defaultMaxBackoffRetryLimit + MaxBackoffRetries *int + // BackoffRetryTimeout specifies time before next retry on 429. When not set, defaults to defaultBackoffTimeout + BackoffRetryTimeout *time.Duration + // ReauthFunc is the function used to re-authenticate the user if the request + // fails with a 401 HTTP response code. This a needed because there may be multiple + // authentication functions for different Identity service versions. + ReauthFunc func() error + + // AKSKAuthOptions provides the value for AK/SK authentication, it should be nil if you use token authentication, + // Otherwise, it must have a value + AKSKAuthOptions AKSKAuthOptions + + mut *sync.RWMutex + + reauthmut *reauthlock +} + +type reauthlock struct { + sync.RWMutex + reauthing bool +} + +// AuthenticatedHeaders returns a map of HTTP headers that are common for all +// authenticated service requests. +func (client *ProviderClient) AuthenticatedHeaders() (m map[string]string) { + if client.reauthmut != nil { + client.reauthmut.RLock() + if client.reauthmut.reauthing { + client.reauthmut.RUnlock() + return + } + client.reauthmut.RUnlock() + } + t := client.Token() + if t == "" { + return + } + return map[string]string{"X-Auth-Token": t} +} + +// UseTokenLock creates a mutex that is used to allow safe concurrent access to the auth token. +// If the application's ProviderClient is not used concurrently, this doesn't need to be called. +func (client *ProviderClient) UseTokenLock() { + client.mut = new(sync.RWMutex) + client.reauthmut = new(reauthlock) +} + +// Token safely reads the value of the auth token from the ProviderClient. Applications should +// call this method to access the token instead of the TokenID field +func (client *ProviderClient) Token() string { + if client.mut != nil { + client.mut.RLock() + defer client.mut.RUnlock() + } + return client.TokenID +} + +// SetToken safely sets the value of the auth token in the ProviderClient. Applications may +// use this method in a custom ReauthFunc +func (client *ProviderClient) SetToken(t string) { + if client.mut != nil { + client.mut.Lock() + defer client.mut.Unlock() + } + client.TokenID = t +} + +// RequestOpts customizes the behavior of the provider.Request() method. +type RequestOpts struct { + // JSONBody, if provided, will be encoded as JSON and used as the body of the HTTP request. The + // content type of the request will default to "application/json" unless overridden by MoreHeaders. + // It's an error to specify both a JSONBody and a RawBody. + JSONBody interface{} + // RawBody contains an io.Reader that will be consumed by the request directly. No content-type + // will be set unless one is provided explicitly by MoreHeaders. + RawBody io.Reader + // JSONResponse, if provided, will be populated with the contents of the response body parsed as JSON. + // Not that setting it will drain and close Response.Body. + // Deprecated: Use http.Response Body instead. + JSONResponse interface{} + // OkCodes contains a list of numeric HTTP status codes that should be interpreted as success. If + // the response has a different code, an error will be returned. + OkCodes []int + // MoreHeaders specifies additional HTTP headers to be provide on the request. If a header is + // provided with a blank value (""), that header will be *omitted* instead: use this to suppress + // the default Accept header or an inferred Content-Type, for example. + MoreHeaders map[string]string + // ErrorContext specifies the resource error type to return if an error is encountered. + // This lets resources override default error messages based on the response status code. + ErrorContext error + + // RetryCount specifies number of times retriable errors (502, 504) will be retried + RetryCount *int + // RetryTimeout specifies time before next retry + RetryTimeout *time.Duration +} + +var applicationJSON = "application/json" + +// Request performs an HTTP request using the ProviderClient's current HTTPClient. An authentication +// header will automatically be provided. +func (client *ProviderClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + var body io.Reader + var contentType *string + + // Derive the content body by either encoding an arbitrary object as JSON, or by taking a provided + // io.ReadSeeker as-is. Default the content-type to application/json. + if options.JSONBody != nil { + if options.RawBody != nil { + panic("Please provide only one of JSONBody or RawBody to golangsdk.Request().") + } + + rendered, err := extract.JsonMarshal(options.JSONBody) + if err != nil { + return nil, err + } + + body = bytes.NewReader(rendered) + contentType = &applicationJSON + } + + if options.RawBody != nil { + body = options.RawBody + } + + // Construct the http.Request. + req, err := http.NewRequest(method, url, body) + if err != nil { + return nil, err + } + + // Populate the request headers. Apply options.MoreHeaders last, to give the caller the chance to + // modify or omit any header. + if contentType != nil { + req.Header.Set("Content-Type", *contentType) + } + req.Header.Set("Accept", applicationJSON) + + // Set the User-Agent header + req.Header.Set("User-Agent", client.UserAgent.Join()) + + if options.MoreHeaders != nil { + for k, v := range options.MoreHeaders { + if v != "" { + req.Header.Set(k, v) + } else { + req.Header.Del(k) + } + } + } + + // get the latest token from client + for k, v := range client.AuthenticatedHeaders() { + req.Header.Set(k, v) + } + + // Set connection parameter to close the connection immediately when we've got the response + req.Close = true + + prereqtok := req.Header.Get("X-Auth-Token") + + if client.AKSKAuthOptions.AccessKey != "" { + Sign(req, SignOptions{ + AccessKey: client.AKSKAuthOptions.AccessKey, + SecretKey: client.AKSKAuthOptions.SecretKey, + }) + if client.AKSKAuthOptions.ProjectId != "" && client.AKSKAuthOptions.DomainID == "" { + req.Header.Set("X-Project-Id", client.AKSKAuthOptions.ProjectId) + } + if client.AKSKAuthOptions.DomainID != "" { + req.Header.Set("X-Domain-Id", client.AKSKAuthOptions.DomainID) + } + if client.AKSKAuthOptions.SecurityToken != "" { + req.Header.Set("X-Security-Token", client.AKSKAuthOptions.SecurityToken) + } + } + + // Issue the request. + resp, err := client.HTTPClient.Do(req) + if err != nil { + return nil, err + } + + // Allow default OkCodes if none explicitly set + if options.OkCodes == nil { + options.OkCodes = defaultOkCodes(method) + } + + if options.RetryCount == nil { + defaultRetryLimit := 1 + options.RetryCount = &defaultRetryLimit + } + + if options.RetryTimeout == nil { + defaultRetryTimeout := 500 * time.Millisecond + options.RetryTimeout = &defaultRetryTimeout + } + + if client.MaxBackoffRetries == nil { + defaultMaxBackoffRetryLimit := 20 + client.MaxBackoffRetries = &defaultMaxBackoffRetryLimit + } + + if client.BackoffRetryTimeout == nil { + defaultBackoffTimeout := 60 * time.Second + client.BackoffRetryTimeout = &defaultBackoffTimeout + } + + // Validate the HTTP response status. + var ok bool + for _, code := range options.OkCodes { + if resp.StatusCode == code { + ok = true + break + } + } + + if !ok { + body, _ := io.ReadAll(resp.Body) + _ = resp.Body.Close() + respErr := ErrUnexpectedResponseCode{ + URL: url, + Method: method, + Expected: options.OkCodes, + Actual: resp.StatusCode, + Body: body, + } + + errType := options.ErrorContext + switch resp.StatusCode { + case http.StatusBadRequest: + err = ErrDefault400{respErr} + if error400er, ok := errType.(Err400er); ok { + err = error400er.Error400(respErr) + } + case http.StatusUnauthorized: + if client.ReauthFunc != nil { + if client.mut != nil { + client.mut.Lock() + client.reauthmut.Lock() + client.reauthmut.reauthing = true + client.reauthmut.Unlock() + if curtok := client.TokenID; curtok == prereqtok { + err = client.ReauthFunc() + } + client.reauthmut.Lock() + client.reauthmut.reauthing = false + client.reauthmut.Unlock() + client.mut.Unlock() + } else { + err = client.ReauthFunc() + } + if err != nil { + e := &ErrUnableToReauthenticate{} + e.ErrOriginal = respErr + return nil, e + } + if options.RawBody != nil { + if seeker, ok := options.RawBody.(io.Seeker); ok { + _, e := seeker.Seek(0, 0) + if e != nil { + return nil, e + } + } + } + resp, err = client.Request(method, url, options) + if err != nil { + e := &ErrErrorAfterReauthentication{} + e.ErrOriginal = err + return nil, e + } + return resp, nil + } + err = ErrDefault401{respErr} + if error401er, ok := errType.(Err401er); ok { + err = error401er.Error401(respErr) + } + case http.StatusForbidden: + err = ErrDefault403{respErr} + if error403er, ok := errType.(Err403er); ok { + err = error403er.Error403(respErr) + } + case http.StatusNotFound: + err = ErrDefault404{respErr} + if error404er, ok := errType.(Err404er); ok { + err = error404er.Error404(respErr) + } + case http.StatusMethodNotAllowed: + err = ErrDefault405{respErr} + if error405er, ok := errType.(Err405er); ok { + err = error405er.Error405(respErr) + } + case http.StatusRequestTimeout: + err = ErrDefault408{respErr} + if error408er, ok := errType.(Err408er); ok { + err = error408er.Error408(respErr) + } + case http.StatusConflict: + err = ErrDefault409{respErr} + if error409er, ok := errType.(Err409er); ok { + err = error409er.Error409(respErr) + } + case http.StatusTooManyRequests: + err = ErrDefault429{respErr} + if error429er, ok := errType.(Err429er); ok { + err = error429er.Error429(respErr) + } + if *client.MaxBackoffRetries > 0 { + *client.MaxBackoffRetries -= 1 + time.Sleep(*client.BackoffRetryTimeout) + return client.Request(method, url, options) + } + case http.StatusInternalServerError: + err = ErrDefault500{respErr} + if error500er, ok := errType.(Err500er); ok { + err = error500er.Error500(respErr) + } + case http.StatusBadGateway, http.StatusGatewayTimeout: // gateway errors + if *options.RetryCount > 0 { + *options.RetryCount -= 1 + time.Sleep(*options.RetryTimeout) + return client.Request(method, url, options) + } + case http.StatusServiceUnavailable: + err = ErrDefault503{respErr} + if error503er, ok := errType.(Err503er); ok { + err = error503er.Error503(respErr) + } + } + + if err == nil { + err = respErr + } + + return resp, err + } + + // Parse the response body as JSON, if requested to do so. + // TODO: When all refactoring of the extract is done, remove this. + if options.JSONResponse != nil && resp.StatusCode != http.StatusNoContent { + switch r := options.JSONResponse.(type) { + case *[]byte: + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("error reading response body: %w", err) + } + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + _ = fmt.Errorf("error in closing : %w", err) + } + }(resp.Body) + + *r = data + default: + if err := extract.Into(resp.Body, &r); err != nil { + return nil, err + } + } + } + + return resp, nil +} + +func defaultOkCodes(method string) []int { + switch method { + case "GET": + return []int{200} + case "POST": + return []int{201, 202} + case "PUT": + return []int{201, 202} + case "PATCH": + return []int{200, 204} + case "DELETE": + return []int{202, 204} + case "HEAD": + return []int{204, 206} + } + return []int{} +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/results.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/results.go new file mode 100644 index 000000000..a86a5ba55 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/results.go @@ -0,0 +1,381 @@ +package golangsdk + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + "time" + + "github.com/opentelekomcloud/gophertelekomcloud/internal/extract" +) + +/* +Result is an internal type to be used by individual resource packages, but its +methods will be available on a wide variety of user-facing embedding types. + +It acts as a base struct that other Result types, returned from request +functions, can embed for convenience. All Results capture basic information +from the HTTP transaction that was performed, including the response body, +HTTP headers, and any errors that happened. + +Generally, each Result type will have an Extract method that can be used to +further interpret the result's payload in a specific context. Extensions or +providers can then provide additional extraction functions to pull out +provider- or extension-specific information as well. + +Deprecated: use functions from internal/extract package instead +*/ +type Result struct { + // Body is the payload of the HTTP response from the server. + Body []byte + + // Header contains the HTTP header structure from the original response. + Header http.Header + + // Err is an error that occurred during the operation. It's deferred until + // extraction to make it easier to chain the Extract call. + Err error +} + +// BodyReader returns cached body as *bytes.Reader +func (r Result) BodyReader() io.Reader { + return bytes.NewReader(r.Body) +} + +type JsonRDSInstanceStatus struct { + Instances []JsonRDSInstanceField `json:"instances"` + TotalCount int `json:"total_count"` +} + +type JsonRDSInstanceField struct { + Id string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` +} + +// ExtractInto allows users to provide an object into which `Extract` will extract +// the `Result.Body`. This would be useful for OpenStack providers that have +// different fields in the response object than OpenStack proper. +// +// Deprecated: use extract.Into function instead +func (r Result) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + return extract.Into(bytes.NewReader(r.Body), to) +} + +// ExtractIntoStructPtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying struct type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +// +// Deprecated: use extract.IntoStructPtr function instead +func (r Result) ExtractIntoStructPtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + return extract.IntoStructPtr(bytes.NewReader(r.Body), to, label) +} + +// ExtractIntoSlicePtr will unmarshal the Result (r) into the provided +// interface{} (to). +// +// NOTE: For internal use only +// +// `to` must be a pointer to an underlying slice type +// +// If provided, `label` will be filtered out of the response +// body prior to `r` being unmarshalled into `to`. +// +// Deprecated: use extract.IntoSlicePtr function instead +func (r Result) ExtractIntoSlicePtr(to interface{}, label string) error { + if r.Err != nil { + return r.Err + } + + return extract.IntoSlicePtr(bytes.NewReader(r.Body), to, label) +} + +func PrettyPrintJSON(body interface{}) string { + pretty, err := json.MarshalIndent(body, "", " ") + if err != nil { + panic(err.Error()) + } + return string(pretty) +} + +// PrettyPrintJSON creates a string containing the full response body as +// pretty-printed JSON. It's useful for capturing test fixtures and for +// debugging extraction bugs. If you include its output in an issue related to +// a buggy extraction function, we will all love you forever. +func (r Result) PrettyPrintJSON() string { + return PrettyPrintJSON(r.Body) +} + +// ErrResult is an internal type to be used by individual resource packages, but +// its methods will be available on a wide variety of user-facing embedding +// types. +// +// It represents results that only contain a potential error and +// nothing else. Usually, if the operation executed successfully, the Err field +// will be nil; otherwise it will be stocked with a relevant error. Use the +// ExtractErr method +// to cleanly pull it out. +// +// Deprecated: use plain err return instead +type ErrResult struct { + Result +} + +// ExtractErr is a function that extracts error information, or nil, from a result. +func (r ErrResult) ExtractErr() error { + return r.Err +} + +// ---------------------------------------------------------------------------- + +type ErrRespond struct { + ErrorCode string `json:"error_code"` + ErrorMsg string `json:"error_msg"` +} + +type ErrWithResult struct { + ErrResult +} + +func (r Result) Extract() (*ErrRespond, error) { + var s = ErrRespond{} + if r.Err != nil { + return nil, r.Err + } + err := r.ExtractInto(&s) + if err != nil { + return nil, fmt.Errorf("failed to extract Error with Result") + } + return &s, nil +} + +// ---------------------------------------------------------------------------- + +/* +HeaderResult is an internal type to be used by individual resource packages, but +its methods will be available on a wide variety of user-facing embedding types. + +It represents a result that only contains an error (possibly nil) and an +http.Header. This is used, for example, by the objectstorage packages in +openstack, because most of the operations don't return response bodies, but do +have relevant information in headers. +*/ +type HeaderResult struct { + Result +} + +// ExtractInto allows users to provide an object into which `Extract` will +// extract the http.Header headers of the result. +func (r HeaderResult) ExtractInto(to interface{}) error { + if r.Err != nil { + return r.Err + } + + tmpHeaderMap := map[string]string{} + for k, v := range r.Header { + if len(v) > 0 { + tmpHeaderMap[k] = v[0] + } + } + + b, err := extract.JsonMarshal(tmpHeaderMap) + if err != nil { + return err + } + err = json.Unmarshal(b, to) + + return err +} + +// RFC3339Milli describes a common time format used by some API responses. +const RFC3339Milli = "2006-01-02T15:04:05.999999Z" + +type JSONRFC3339Milli time.Time + +func (jt *JSONRFC3339Milli) UnmarshalJSON(data []byte) error { + b := bytes.NewBuffer(data) + dec := json.NewDecoder(b) + var s string + if err := dec.Decode(&s); err != nil { + return err + } + t, err := time.Parse(RFC3339Milli, s) + if err != nil { + return err + } + *jt = JSONRFC3339Milli(t) + return nil +} + +const RFC3339MilliNoZ = "2006-01-02T15:04:05.999999" + +type JSONRFC3339MilliNoZ time.Time + +func (jt *JSONRFC3339MilliNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339MilliNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339MilliNoZ(t) + return nil +} + +type JSONRFC1123 time.Time + +func (jt *JSONRFC1123) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(time.RFC1123, s) + if err != nil { + return err + } + *jt = JSONRFC1123(t) + return nil +} + +type JSONUnix time.Time + +func (jt *JSONUnix) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + unix, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + t = time.Unix(unix, 0) + *jt = JSONUnix(t) + return nil +} + +// RFC3339NoZ is the time format used in Heat (Orchestration). +const RFC3339NoZ = "2006-01-02T15:04:05" + +type JSONRFC3339NoZ time.Time + +func (jt *JSONRFC3339NoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339NoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339NoZ(t) + return nil +} + +// RFC3339ZNoT is the time format used in Zun (Containers Service). +const RFC3339ZNoT = "2006-01-02 15:04:05-07:00" + +type JSONRFC3339ZNoT time.Time + +func (jt *JSONRFC3339ZNoT) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoT, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoT(t) + return nil +} + +// RFC3339ZNoTNoZ is another time format used in Zun (Containers Service). +const RFC3339ZNoTNoZ = "2006-01-02 15:04:05" + +type JSONRFC3339ZNoTNoZ time.Time + +func (jt *JSONRFC3339ZNoTNoZ) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + if s == "" { + return nil + } + t, err := time.Parse(RFC3339ZNoTNoZ, s) + if err != nil { + return err + } + *jt = JSONRFC3339ZNoTNoZ(t) + return nil +} + +/* +Link is an internal type to be used in packages of collection resources that are +paginated in a certain way. + +It's a response substructure common to many paginated collection results that is +used to point to related pages. Usually, the one we care about is the one with +Rel field set to "next". +*/ +type Link struct { + Href string `json:"href"` + Rel string `json:"rel"` +} + +/* +ExtractNextURL is an internal function useful for packages of collection +resources that are paginated in a certain way. + +It attempts to extract the "next" URL from slice of Link structs, or +"" if no such URL is present. +*/ +func ExtractNextURL(links []Link) (string, error) { + var url string + + for _, l := range links { + if l.Rel == "next" { + url = l.Href + } + } + + if url == "" { + return "", nil + } + + return url, nil +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/results_job.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/results_job.go new file mode 100644 index 000000000..5c844dca2 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/results_job.go @@ -0,0 +1,100 @@ +package golangsdk + +import ( + "fmt" + "strings" +) + +type JobResponse struct { + URI string `json:"uri"` + JobID string `json:"job_id"` +} + +type JobStatus struct { + Status string `json:"status"` + Entities map[string]interface{} `json:"entities"` + JobID string `json:"job_id"` + JobType string `json:"job_type"` + ErrorCode string `json:"error_code"` + FailReason string `json:"fail_reason"` +} + +type RDSJobStatus struct { + Job Job `json:"job"` +} + +type Job struct { + Id string `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + Created string `json:"created"` + Process string `json:"process"` + Instance RDSJobInstance `json:"instance"` +} + +type RDSJobInstance struct { + Id string `json:"id"` + Name string `json:"name"` +} + +func (r Result) ExtractJobResponse() (*JobResponse, error) { + job := new(JobResponse) + err := r.ExtractInto(job) + return job, err +} + +func (r Result) ExtractJobStatus() (*JobStatus, error) { + job := new(JobStatus) + err := r.ExtractInto(job) + return job, err +} + +func GetJobEndpoint(endpoint string) string { + n := strings.Index(endpoint[8:], "/") + if n == -1 { + return endpoint + } + return endpoint[0 : n+8] +} + +func WaitForJobSuccess(client *ServiceClient, uri string, secs int) error { + uri = strings.Replace(uri, "v1", "v1.0", 1) + + return WaitFor(secs, func() (bool, error) { + job := new(JobStatus) + _, err := client.Get(GetJobEndpoint(client.Endpoint)+uri, &job, nil) + if err != nil { + return false, err + } + fmt.Printf("JobStatus: %+v.\n", job) + + if job.Status == "SUCCESS" { + return true, nil + } + if job.Status == "FAIL" { + err = fmt.Errorf("Job failed with code %s: %s.\n", job.ErrorCode, job.FailReason) + return false, err + } + + return false, nil + }) +} + +func GetJobEntity(client *ServiceClient, uri string, label string) (interface{}, error) { + uri = strings.Replace(uri, "v1", "v1.0", 1) + + job := new(JobStatus) + _, err := client.Get(GetJobEndpoint(client.Endpoint)+uri, &job, nil) + if err != nil { + return nil, err + } + fmt.Printf("JobStatus: %+v.\n", job) + + if job.Status == "SUCCESS" { + if e := job.Entities[label]; e != nil { + return e, nil + } + } + + return nil, fmt.Errorf("unexpected conversion error in GetJobEntity") +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client.go new file mode 100644 index 000000000..ac4c24e18 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client.go @@ -0,0 +1,183 @@ +package golangsdk + +import ( + "io" + "net/http" + "strings" +) + +// ServiceClient stores details required to interact with a specific service API implemented by a provider. +// Generally, you'll acquire these by calling the appropriate `New` method on a ProviderClient. +type ServiceClient struct { + // ProviderClient is a reference to the provider that implements this service. + *ProviderClient + + // Endpoint is the base URL of the service's API, acquired from a service catalog. + // It MUST end with a /. + Endpoint string + + // ResourceBase is the base URL shared by the resources within a service's API. It should include + // the API version and, like Endpoint, MUST end with a / if set. If not set, the Endpoint is used + // as-is, instead. + ResourceBase string + + // This is the service client type (e.g. compute, sharev2). + // NOTE: FOR INTERNAL USE ONLY. DO NOT SET. GOPHERCLOUD WILL SET THIS. + // It is only exported because it gets set in a different package. + Type string + + // The microversion of the service to use. Set this to use a particular microversion. + Microversion string + + // MoreHeaders allows users to set service-wide headers on requests. Put another way, + // values set in this field will be set on all the HTTP requests the service client sends. + MoreHeaders map[string]string +} + +// ResourceBaseURL returns the base URL of any resources used by this service. It MUST end with a /. +func (client *ServiceClient) ResourceBaseURL() string { + if client.ResourceBase != "" { + return client.ResourceBase + } + return client.Endpoint +} + +// ServiceURL constructs a URL for a resource belonging to this provider. +func (client *ServiceClient) ServiceURL(parts ...string) string { + return client.ResourceBaseURL() + strings.Join(parts, "/") +} + +func (client *ServiceClient) initReqOpts(_ string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) { + if v, ok := (JSONBody).(io.Reader); ok { + opts.RawBody = v + } else if JSONBody != nil { + opts.JSONBody = JSONBody + } + + if JSONResponse != nil { + opts.JSONResponse = JSONResponse + } + + if opts.MoreHeaders == nil { + opts.MoreHeaders = make(map[string]string) + } + + if client.Microversion != "" { + client.setMicroversionHeader(opts) + } +} + +// Get calls `Request` with the "GET" HTTP verb. Def 200 +// JSONResponse Deprecated +func (client *ServiceClient) Get(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, JSONResponse, opts) + return client.Request("GET", url, opts) +} + +// Post calls `Request` with the "POST" HTTP verb. Def 201, 202 +// JSONResponse Deprecated +func (client *ServiceClient) Post(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("POST", url, opts) +} + +// Put calls `Request` with the "PUT" HTTP verb. Def 201, 202 +// JSONResponse Deprecated +func (client *ServiceClient) Put(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PUT", url, opts) +} + +// Patch calls `Request` with the "PATCH" HTTP verb. Def 200, 204 +// JSONResponse Deprecated +func (client *ServiceClient) Patch(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("PATCH", url, opts) +} + +// Delete calls `Request` with the "DELETE" HTTP verb. Def 202, 204 +func (client *ServiceClient) Delete(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("DELETE", url, opts) +} + +// Head calls `Request` with the "HEAD" HTTP verb. Def 204, 206 +func (client *ServiceClient) Head(url string, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, nil, opts) + return client.Request("HEAD", url, opts) +} + +// DeleteWithBody calls `Request` with the "DELETE" HTTP verb. Def 202, 204 +func (client *ServiceClient) DeleteWithBody(url string, JSONBody interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, nil, opts) + return client.Request("DELETE", url, opts) +} + +// DeleteWithResponse calls `Request` with the "DELETE" HTTP verb. Def 202, 204 +// Deprecated +func (client *ServiceClient) DeleteWithResponse(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, JSONResponse, opts) + return client.Request("DELETE", url, opts) +} + +// DeleteWithBodyResp calls `Request` with the "DELETE" HTTP verb. Def 202, 204 +// Deprecated +func (client *ServiceClient) DeleteWithBodyResp(url string, JSONBody interface{}, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, JSONBody, JSONResponse, opts) + return client.Request("DELETE", url, opts) +} + +func (client *ServiceClient) setMicroversionHeader(opts *RequestOpts) { + switch client.Type { + case "compute": + opts.MoreHeaders["X-OpenStack-Nova-API-Version"] = client.Microversion + case "sharev2": + opts.MoreHeaders["X-OpenStack-Manila-API-Version"] = client.Microversion + case "volume": + opts.MoreHeaders["X-OpenStack-Volume-API-Version"] = client.Microversion + } + + if client.Type != "" { + opts.MoreHeaders["OpenStack-API-Version"] = client.Type + " " + client.Microversion + } +} + +// Request carries out the HTTP operation for the service client +func (client *ServiceClient) Request(method, url string, options *RequestOpts) (*http.Response, error) { + if len(client.MoreHeaders) > 0 { + if options == nil { + options = new(RequestOpts) + } + for k, v := range client.MoreHeaders { + options.MoreHeaders[k] = v + } + } + return client.ProviderClient.Request(method, url, options) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client_extension.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client_extension.go new file mode 100644 index 000000000..d59a82835 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/service_client_extension.go @@ -0,0 +1,23 @@ +package golangsdk + +import ( + "net/http" +) + +type ServiceClientExtension struct { + + // ServiceClient is a reference to the ServiceClient. + *ServiceClient + + // ProjectID is the ID of project to which User is authorized. + ProjectID string +} + +// Delete calls `Request` with the "DELETE" HTTP verb. +func (client *ServiceClient) Delete2(url string, JSONResponse interface{}, opts *RequestOpts) (*http.Response, error) { + if opts == nil { + opts = new(RequestOpts) + } + client.initReqOpts(url, nil, JSONResponse, opts) + return client.Request("DELETE", url, opts) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/signer_helper.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/signer_helper.go new file mode 100644 index 000000000..c4f745020 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/signer_helper.go @@ -0,0 +1,519 @@ +package golangsdk + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/textproto" + "net/url" + "regexp" + "sort" + "strings" + "sync" + "time" +) + +// MemoryCache presents a thread safe memory cache +type MemoryCache struct { + sync.Mutex // handling r/w for cache + cacheHolder map[string]string // cache holder + cacheKeys []string // cache keys + MaxCount int // max cache entry count +} + +// NewCache inits an new MemoryCache +func NewCache(maxCount int) *MemoryCache { + return &MemoryCache{ + cacheHolder: make(map[string]string, maxCount), + MaxCount: maxCount, + } +} + +// Add an new cache item +func (cache *MemoryCache) Add(cacheKey string, cacheData string) { + cache.Lock() + defer cache.Unlock() + + if len(cache.cacheKeys) >= cache.MaxCount && len(cache.cacheKeys) > 1 { + delete(cache.cacheHolder, cache.cacheKeys[0]) // delete first item + cache.cacheKeys = cache.cacheKeys[1:] // pop first one + } + + cache.cacheHolder[cacheKey] = cacheData + cache.cacheKeys = append(cache.cacheKeys, cacheKey) +} + +// Get a cache item by its key +func (cache *MemoryCache) Get(cacheKey string) string { + cache.Lock() + defer cache.Unlock() + + return cache.cacheHolder[cacheKey] +} + +// caseInsensitiveStringArray represents string case insensitive sorting operations +type caseInsensitiveStringArray []string + +// noEscape specifies whether the character should be encoded or not +var noEscape [256]bool + +func init() { + // refer to https://docs.oracle.com/javase/7/docs/api/java/net/URLEncoder.html + for i := 0; i < len(noEscape); i++ { + noEscape[i] = (i >= 'A' && i <= 'Z') || + (i >= 'a' && i <= 'z') || + (i >= '0' && i <= '9') || + i == '.' || + i == '-' || + i == '_' || + i == '~' // java-sdk-core 3.0.1 HttpUtils.urlEncode + } +} + +// SignOptions represents the options during signing http request, it is concurency safely +type SignOptions struct { + AccessKey string // Access Key + SecretKey string // Secret key + RegionName string // Region name + ServiceName string // Service Name + EnableCacheSignKey bool // Cache sign key for one day or not cache, cache is disabled by default + encodeUrl bool // internal use + SignAlgorithm string // The algorithm used for sign, the default value is "SDK-HMAC-SHA256" if you don't set its value + TimeOffsetInSeconds int64 // TimeOffsetInSeconds is used for adjust x-sdk-date if set its value +} + +// StringBuilder wraps bytes.Buffer to implement a high performance string builder +type StringBuilder struct { + builder bytes.Buffer // string storage +} + +// reqSignParams represents the option values used for signing http request +type reqSignParams struct { + SignOptions + RequestTime time.Time + Req *http.Request +} + +// signKeyCacheEntry represents the cache entry of sign key +type signKeyCacheEntry struct { + Key []byte // sign key + NumberOfDaysSinceEpoch int64 // number of days since epoch +} + +// The default sign algorithm +const SignAlgorithmHMACSHA256 = "SDK-HMAC-SHA256" + +// The header key of content hash value +const ContentSha256HeaderKey = "x-sdk-content-sha256" + +// A regular for searching empty string +var spaceRegexp = regexp.MustCompile(`\s+`) + +// cache sign key +var cache = NewCache(300) + +// Sign manipulates the http.Request instance with some required authentication headers for SK/SK auth +func Sign(req *http.Request, signOptions SignOptions) { + signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey) + signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey) + signOptions.encodeUrl = true + + signParams := reqSignParams{ + SignOptions: signOptions, + RequestTime: time.Now(), + Req: req, + } + + // t, _ := time.Parse(time.RFC3339, "2018-04-15T04:28:22+00:00") + // signParams.RequestTime = t + + if signParams.SignAlgorithm == "" { + signParams.SignAlgorithm = SignAlgorithmHMACSHA256 + } + + addRequiredHeaders(req, signParams.getFormattedSigningDateTime()) + contentSha256 := "" + + if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok { + contentSha256 = calculateContentHash(req) + } else { + contentSha256 = v[0] + } + + canonicalRequest := createCanonicalRequest(signParams, contentSha256) + + /*fmt.Println("canonicalRequest: " + canonicalRequest) + fmt.Println("*****")*/ + + strToSign := createStringToSign(canonicalRequest, signParams) + signKey := deriveSigningKey(signParams) + signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm) + + req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature)) +} + +// ReSign manipulates the http.Request instance with some required authentication headers for SK/SK auth +func ReSign(req *http.Request, signOptions SignOptions) { + signOptions.AccessKey = strings.TrimSpace(signOptions.AccessKey) + signOptions.SecretKey = strings.TrimSpace(signOptions.SecretKey) + signOptions.encodeUrl = true + + signParams := reqSignParams{ + SignOptions: signOptions, + RequestTime: time.Now(), + Req: req, + } + + if signParams.SignAlgorithm == "" { + signParams.SignAlgorithm = SignAlgorithmHMACSHA256 + } + + setRequiredHeaders(req, signParams.getFormattedSigningDateTime()) + contentSha256 := "" + + if v, ok := req.Header[textproto.CanonicalMIMEHeaderKey(ContentSha256HeaderKey)]; !ok { + contentSha256 = calculateContentHash(req) + } else { + contentSha256 = v[0] + } + + canonicalRequest := createCanonicalRequest(signParams, contentSha256) + + strToSign := createStringToSign(canonicalRequest, signParams) + signKey := deriveSigningKey(signParams) + signature := computeSignature(strToSign, signKey, signParams.SignAlgorithm) + + req.Header.Set("Authorization", buildAuthorizationHeader(signParams, signature)) +} + +// deriveSigningKey returns a sign key from cache, or build it and insert it into cache +func deriveSigningKey(signParam reqSignParams) []byte { + if signParam.EnableCacheSignKey { + cacheKey := strings.Join([]string{signParam.SecretKey, + signParam.RegionName, + signParam.ServiceName, + }, "-") + + cacheData := cache.Get(cacheKey) + + if cacheData != "" { + var signKey signKeyCacheEntry + _ = json.Unmarshal([]byte(cacheData), &signKey) + + if signKey.NumberOfDaysSinceEpoch == signParam.getDaysSinceEpon() { + return signKey.Key + } + } + + signKey := buildSignKey(signParam) + signKeyStr, _ := json.Marshal(signKeyCacheEntry{ + Key: signKey, + NumberOfDaysSinceEpoch: signParam.getDaysSinceEpon(), + }) + cache.Add(cacheKey, string(signKeyStr)) + return signKey + } else { + return buildSignKey(signParam) + } +} + +func buildSignKey(signParam reqSignParams) []byte { + var kSecret StringBuilder + kSecret.Write("SDK").Write(signParam.SecretKey) + + kDate := computeSignature(signParam.getFormattedSigningDate(), kSecret.GetBytes(), signParam.SignAlgorithm) + kRegion := computeSignature(signParam.RegionName, kDate, signParam.SignAlgorithm) + kService := computeSignature(signParam.ServiceName, kRegion, signParam.SignAlgorithm) + return computeSignature("sdk_request", kService, signParam.SignAlgorithm) +} + +// HmacSha256 implements the Keyed-Hash Message Authentication Code computation +func HmacSha256(data string, key []byte) []byte { + mac := hmac.New(sha256.New, key) + _, _ = mac.Write([]byte(data)) + return mac.Sum(nil) +} + +// HashSha256 is a wrapper for sha256 implementation +func HashSha256(msg []byte) []byte { + sh256 := sha256.New() + _, _ = sh256.Write(msg) + + return sh256.Sum(nil) +} + +// buildAuthorizationHeader builds the authentication header value +func buildAuthorizationHeader(signParam reqSignParams, signature []byte) string { + var signingCredentials StringBuilder + signingCredentials.Write(signParam.AccessKey).Write("/").Write(signParam.getScope()) + + credential := "Credential=" + signingCredentials.ToString() + signerHeaders := "SignedHeaders=" + getSignedHeadersString(signParam.Req) + signatureHeader := "Signature=" + hex.EncodeToString(signature) + + return signParam.SignAlgorithm + " " + strings.Join([]string{ + credential, + signerHeaders, + signatureHeader, + }, ", ") +} + +// computeSignature computers the signature with the specified algorithm +// and it only supports SDK-HMAC-SHA256 in currently +func computeSignature(signData string, key []byte, algorithm string) []byte { + if algorithm == SignAlgorithmHMACSHA256 { + return HmacSha256(signData, key) + } else { + log.Fatalf("Unsupported algorithm %s, please use %s and try again", algorithm, SignAlgorithmHMACSHA256) + return nil + } +} + +// createStringToSign build the need to be signed string +func createStringToSign(canonicalRequest string, signParams reqSignParams) string { + return strings.Join([]string{signParams.SignAlgorithm, + signParams.getFormattedSigningDateTime(), + signParams.getScope(), + hex.EncodeToString(HashSha256([]byte(canonicalRequest))), + }, "\n") +} + +// getCanonicalizedResourcePath builds the valid url path for signing +func getCanonicalizedResourcePath(signParas reqSignParams) string { + urlStr := signParas.Req.URL.Path + if !strings.HasPrefix(urlStr, "/") { + urlStr = "/" + urlStr + } + + if !strings.HasSuffix(urlStr, "/") { + urlStr = urlStr + "/" + } + + if signParas.encodeUrl { + urlStr = urlEncode(urlStr, true) + } + + if urlStr == "" { + urlStr = "/" + } + + return urlStr +} + +// urlEncode encodes url path and url querystring according to the following rules: +// The alphanumeric characters "a" through "z", "A" through "Z" and "0" through "9" remain the same. +// The special characters ".", "-", "*", and "_" remain the same. +// The space character " " is converted into a plus sign "%20". +// All other characters are unsafe and are first converted into one or more bytes using some encoding scheme. +func urlEncode(url string, urlPath bool) string { + var buf bytes.Buffer + for i := 0; i < len(url); i++ { + c := url[i] + if noEscape[c] || (c == '/' && urlPath) { + buf.WriteByte(c) + } else { + _, _ = fmt.Fprintf(&buf, "%%%02X", c) + } + } + + return buf.String() +} + +// encodeQueryString build and encode querystring to a string for signing +func encodeQueryString(queryValues url.Values) string { + var encodedVals = make(map[string]string, len(queryValues)) + var keys = make([]string, len(queryValues)) + + i := 0 + + for k := range queryValues { + keys[i] = urlEncode(k, false) + encodedVals[keys[i]] = k + i++ + } + + caseInsensitiveSort(keys) + + var queryStr StringBuilder + for i, k := range keys { + if i > 0 { + queryStr.Write("&") + } + + queryStr.Write(k).Write("=").Write(urlEncode(queryValues.Get(encodedVals[k]), false)) + } + + return queryStr.ToString() +} + +// getCanonicalizedQueryString return empty string if in POST method and content is nil, otherwise returns sorted,encoded querystring +func getCanonicalizedQueryString(signParas reqSignParams) string { + if usePayloadForQueryParameters(signParas.Req) { + return "" + } else { + return encodeQueryString(signParas.Req.URL.Query()) + } +} + +// createCanonicalRequest builds canonical string depends the official document for signing +func createCanonicalRequest(signParas reqSignParams, contentSha256 string) string { + return strings.Join([]string{signParas.Req.Method, + getCanonicalizedResourcePath(signParas), + getCanonicalizedQueryString(signParas), + getCanonicalizedHeaderString(signParas.Req), + getSignedHeadersString(signParas.Req), + contentSha256, + }, "\n") +} + +// calculateContentHash computes the content hash value +func calculateContentHash(req *http.Request) string { + encodeParas := "" + + // post and content is null use queryString as content -- according to document + if usePayloadForQueryParameters(req) { + encodeParas = req.URL.Query().Encode() + } else { + if req.Body == nil { + encodeParas = "" + } else { + readBody, _ := ioutil.ReadAll(req.Body) + req.Body = ioutil.NopCloser(bytes.NewBuffer(readBody)) + encodeParas = string(readBody) + } + } + + return hex.EncodeToString(HashSha256([]byte(encodeParas))) +} + +// usePayloadForQueryParameters specifies use querystring or not as content for compute content hash +func usePayloadForQueryParameters(req *http.Request) bool { + if strings.ToLower(req.Method) != "post" { + return false + } + + return req.Body == nil +} + +// getCanonicalizedHeaderString converts header map to a string for signing +func getCanonicalizedHeaderString(req *http.Request) string { + var headers StringBuilder + + keys := make([]string, 0) + for k := range req.Header { + keys = append(keys, strings.TrimSpace(k)) + } + + caseInsensitiveSort(keys) + + for _, k := range keys { + k = strings.ToLower(k) + newKey := spaceRegexp.ReplaceAllString(k, " ") + headers.Write(newKey) + headers.Write(":") + + val := req.Header.Get(k) + val = spaceRegexp.ReplaceAllString(val, " ") + headers.Write(val) + + headers.Write("\n") + } + + return headers.ToString() +} + +// getSignedHeadersString builds the string for AuthorizationHeader and signing +func getSignedHeadersString(req *http.Request) string { + var headers StringBuilder + + keys := make([]string, 0) + for k := range req.Header { + keys = append(keys, strings.TrimSpace(k)) + } + + caseInsensitiveSort(keys) + + for idx, k := range keys { + + if idx > 0 { + headers.Write(";") + } + + headers.Write(strings.ToLower(k)) + } + + return headers.ToString() +} + +// addRequiredHeaders adds the required heads to http.request instance +func addRequiredHeaders(req *http.Request, timeStr string) { + // golang handles port by default + req.Header.Add("Host", req.URL.Host) + req.Header.Add("X-Sdk-Date", timeStr) +} + +// setRequiredHeaders sets the required heads to http.request for redirection +func setRequiredHeaders(req *http.Request, timeStr string) { + req.Header.Set("X-Sdk-Date", timeStr) + req.Header.Del("Authorization") +} + +func (s caseInsensitiveStringArray) Len() int { + return len(s) +} +func (s caseInsensitiveStringArray) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} +func (s caseInsensitiveStringArray) Less(i, j int) bool { + return strings.ToLower(s[i]) < strings.ToLower(s[j]) +} + +func caseInsensitiveSort(strSlice []string) { + sort.Sort(caseInsensitiveStringArray(strSlice)) +} + +func (signParas *reqSignParams) getSigningDateTimeMilli() int64 { + return (signParas.RequestTime.UTC().Unix() - signParas.TimeOffsetInSeconds) * 1000 +} + +func (signParas *reqSignParams) getSigningDateTime() time.Time { + return time.Unix(signParas.getSigningDateTimeMilli()/1000, 0) +} + +func (signParas *reqSignParams) getDaysSinceEpon() int64 { + return signParas.getSigningDateTimeMilli() / 1000 / 3600 / 24 +} + +func (signParas *reqSignParams) getFormattedSigningDate() string { + return signParas.getSigningDateTime().UTC().Format("20060102") +} +func (signParas *reqSignParams) getFormattedSigningDateTime() string { + return signParas.getSigningDateTime().UTC().Format("20060102T150405Z") +} + +func (signParas *reqSignParams) getScope() string { + return strings.Join([]string{signParas.getFormattedSigningDate(), + signParas.RegionName, + signParas.ServiceName, + "sdk_request", + }, "/") +} + +func (buff *StringBuilder) Write(s string) *StringBuilder { + buff.builder.WriteString(s) + return buff +} + +func (buff *StringBuilder) ToString() string { + return buff.builder.String() +} + +func (buff *StringBuilder) GetBytes() []byte { + return []byte(buff.ToString()) +} diff --git a/vendor/github.com/opentelekomcloud/gophertelekomcloud/util.go b/vendor/github.com/opentelekomcloud/gophertelekomcloud/util.go new file mode 100644 index 000000000..8e59c9182 --- /dev/null +++ b/vendor/github.com/opentelekomcloud/gophertelekomcloud/util.go @@ -0,0 +1,100 @@ +package golangsdk + +import ( + "fmt" + "net/url" + "path/filepath" + "strings" + "time" +) + +// WaitFor polls a predicate function, once per second, up to a timeout limit. +// This is useful to wait for a resource to transition to a certain state. +// To handle situations when the predicate might hang indefinitely, the +// predicate will be prematurely cancelled after the timeout. +// Resource packages will wrap this in a more convenient function that's +// specific to a certain resource, but it can also be useful on its own. +func WaitFor(timeout int, predicate func() (bool, error)) error { + type WaitForResult struct { + Success bool + Error error + } + + start := time.Now().Unix() + + for { + // If a timeout is set, and that's been exceeded, shut it down. + if timeout >= 0 && time.Now().Unix()-start >= int64(timeout) { + return fmt.Errorf("A timeout occurred") + } + + time.Sleep(1 * time.Second) + + var result WaitForResult + ch := make(chan bool, 1) + go func() { + defer close(ch) + satisfied, err := predicate() + result.Success = satisfied + result.Error = err + }() + + select { + case <-ch: + if result.Error != nil { + return result.Error + } + if result.Success { + return nil + } + // If the predicate has not finished by the timeout, cancel it. + case <-time.After(time.Duration(timeout) * time.Second): + return fmt.Errorf("A timeout occurred") + } + } +} + +// NormalizeURL is an internal function to be used by provider clients. +// +// It ensures that each endpoint URL has a closing `/`, as expected by +// ServiceClient's methods. +func NormalizeURL(url string) string { + if !strings.HasSuffix(url, "/") { + return url + "/" + } + return url +} + +// NormalizePathURL is used to convert rawPath to a fqdn, using basePath as +// a reference in the filesystem, if necessary. basePath is assumed to contain +// either '.' when first used, or the file:// type fqdn of the parent resource. +// e.g. myFavScript.yaml => file://opt/lib/myFavScript.yaml +func NormalizePathURL(basePath, rawPath string) (string, error) { + u, err := url.Parse(rawPath) + if err != nil { + return "", err + } + // if a scheme is defined, it must be a fqdn already + if u.Scheme != "" { + return u.String(), nil + } + // if basePath is a url, then child resources are assumed to be relative to it + bu, err := url.Parse(basePath) + if err != nil { + return "", err + } + var basePathSys, absPathSys string + if bu.Scheme != "" { + basePathSys = filepath.FromSlash(bu.Path) + absPathSys = filepath.Join(basePathSys, rawPath) + bu.Path = filepath.ToSlash(absPathSys) + return bu.String(), nil + } + + absPathSys = filepath.Join(basePath, rawPath) + u.Path = filepath.ToSlash(absPathSys) + + u.Scheme = "file" + return u.String(), nil + +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 84f87eb44..c44a0f3c2 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -741,6 +741,20 @@ github.com/opencontainers/image-spec/specs-go/v1 # github.com/opencontainers/runc v1.1.14 ## explicit; go 1.18 github.com/opencontainers/runc/libcontainer/user +# github.com/opentelekomcloud/gophertelekomcloud v0.9.3 +## explicit; go 1.20 +github.com/opentelekomcloud/gophertelekomcloud +github.com/opentelekomcloud/gophertelekomcloud/internal/build +github.com/opentelekomcloud/gophertelekomcloud/internal/extract +github.com/opentelekomcloud/gophertelekomcloud/internal/multierr +github.com/opentelekomcloud/gophertelekomcloud/openstack +github.com/opentelekomcloud/gophertelekomcloud/openstack/cce/v3/clusters +github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/catalog +github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/domains +github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/projects +github.com/opentelekomcloud/gophertelekomcloud/openstack/identity/v3/tokens +github.com/opentelekomcloud/gophertelekomcloud/openstack/utils +github.com/opentelekomcloud/gophertelekomcloud/pagination # github.com/ovh/go-ovh v1.4.3 ## explicit; go 1.18 github.com/ovh/go-ovh/ovh