autoscaler/cluster-autoscaler/cloudprovider/ovhcloud/ovh_cloud_manager.go

287 lines
9.4 KiB
Go

/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ovhcloud
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"time"
"k8s.io/autoscaler/cluster-autoscaler/cloudprovider/ovhcloud/sdk"
"k8s.io/klog/v2"
)
const flavorCacheDuration = time.Hour
// ClientInterface defines all mandatory methods to be exposed as a client (mock or API)
type ClientInterface interface {
// ListNodePools lists all the node pools found in a Kubernetes cluster.
ListNodePools(ctx context.Context, projectID string, clusterID string) ([]sdk.NodePool, error)
// ListNodePoolNodes lists all the nodes contained in a node pool.
ListNodePoolNodes(ctx context.Context, projectID string, clusterID string, poolID string) ([]sdk.Node, error)
// CreateNodePool fills and installs a new pool in a Kubernetes cluster.
CreateNodePool(ctx context.Context, projectID string, clusterID string, opts *sdk.CreateNodePoolOpts) (*sdk.NodePool, error)
// UpdateNodePool updates the details of an existing node pool.
UpdateNodePool(ctx context.Context, projectID string, clusterID string, poolID string, opts *sdk.UpdateNodePoolOpts) (*sdk.NodePool, error)
// DeleteNodePool deletes a specific pool.
DeleteNodePool(ctx context.Context, projectID string, clusterID string, poolID string) (*sdk.NodePool, error)
// ListClusterFlavors list all available flavors usable in a Kubernetes cluster.
ListClusterFlavors(ctx context.Context, projectID string, clusterID string) ([]sdk.Flavor, error)
}
// OvhCloudManager defines current application context manager to interact
// with resources and API (or mock)
type OvhCloudManager struct {
Client ClientInterface
OpenStackProvider *sdk.OpenStackProvider
ClusterID string
ProjectID string
NodePools []sdk.NodePool
NodeGroupPerProviderID map[string]*NodeGroup
FlavorsCache map[string]sdk.Flavor
FlavorsCacheExpirationTime time.Time
}
// Config is the configuration file content of OVHcloud provider
type Config struct {
// ProjectID is the id associated with the cluster project tenant.
ProjectID string `json:"project_id"`
// ClusterID is the id associated with the cluster where CA is running.
ClusterID string `json:"cluster_id"`
// AuthenticationType is the authentication method used to call the API (should be openstack or consumer)
AuthenticationType string `json:"authentication_type"`
// OpenStack keystone credentials if CA is run without API consumer.
// By default, this is used as it on cluster control plane.
OpenStackAuthUrl string `json:"openstack_auth_url"`
OpenStackUsername string `json:"openstack_username"`
OpenStackPassword string `json:"openstack_password"`
OpenStackDomain string `json:"openstack_domain"`
// Application credentials if CA is run as API consumer without using OpenStack keystone.
// Tokens can be created here: https://api.ovh.com/createToken/
ApplicationEndpoint string `json:"application_endpoint"`
ApplicationKey string `json:"application_key"`
ApplicationSecret string `json:"application_secret"`
ApplicationConsumerKey string `json:"application_consumer_key"`
}
// Authentication methods defines the way to interact with API.
const (
// OpenStackAuthenticationType to request a keystone token credentials.
OpenStackAuthenticationType = "openstack"
// ApplicationConsumerAuthenticationType to consume an application key credentials.
ApplicationConsumerAuthenticationType = "consumer"
)
// NewManager initializes an API client given a cloud provider configuration file
func NewManager(configFile io.Reader) (*OvhCloudManager, error) {
var client ClientInterface
var openStackProvider *sdk.OpenStackProvider
// First, read configuration file to properly boot API client
cfg, err := readConfig(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err)
}
// Then, validate payload
err = validatePayload(cfg)
if err != nil {
return nil, fmt.Errorf("config content validation failed: %w", err)
}
// Eventually, create API client given its authentication method
switch cfg.AuthenticationType {
case OpenStackAuthenticationType:
openStackProvider, err = sdk.NewOpenStackProvider(cfg.OpenStackAuthUrl, cfg.OpenStackUsername, cfg.OpenStackPassword, cfg.OpenStackDomain, cfg.ProjectID)
if err != nil {
return nil, fmt.Errorf("failed to create OpenStack provider: %w", err)
}
client, err = sdk.NewDefaultClientWithToken(openStackProvider.AuthUrl, openStackProvider.Token)
case ApplicationConsumerAuthenticationType:
client, err = sdk.NewClient(cfg.ApplicationEndpoint, cfg.ApplicationKey, cfg.ApplicationSecret, cfg.ApplicationConsumerKey)
default:
err = errors.New("authentication method unknown")
}
if err != nil {
return nil, fmt.Errorf("failed to create API client: %w", err)
}
return &OvhCloudManager{
Client: client,
OpenStackProvider: openStackProvider,
ProjectID: cfg.ProjectID,
ClusterID: cfg.ClusterID,
NodePools: make([]sdk.NodePool, 0),
NodeGroupPerProviderID: make(map[string]*NodeGroup),
FlavorsCache: make(map[string]sdk.Flavor),
FlavorsCacheExpirationTime: time.Time{},
}, nil
}
// getFlavorsByName lists available flavors from cache or from OVHCloud APIs if the cache is outdated
func (m *OvhCloudManager) getFlavorsByName() (map[string]sdk.Flavor, error) {
// Update the flavors cache if expired
if m.FlavorsCacheExpirationTime.Before(time.Now()) {
newFlavorCacheExpirationTime := time.Now().Add(flavorCacheDuration)
klog.V(4).Infof("Listing flavors to update flavors cache (will expire at %s)", newFlavorCacheExpirationTime)
// Fetch all flavors in API
flavors, err := m.Client.ListClusterFlavors(context.Background(), m.ProjectID, m.ClusterID)
if err != nil {
return nil, fmt.Errorf("failed to list available flavors: %w", err)
}
// Update the flavors cache
m.FlavorsCache = make(map[string]sdk.Flavor)
for _, flavor := range flavors {
m.FlavorsCache[flavor.Name] = flavor
m.FlavorsCacheExpirationTime = newFlavorCacheExpirationTime
}
}
return m.FlavorsCache, nil
}
// getFlavorByName returns the given flavor from cache or API
func (m *OvhCloudManager) getFlavorByName(flavorName string) (sdk.Flavor, error) {
flavorsByName, err := m.getFlavorsByName()
if err != nil {
return sdk.Flavor{}, err
}
if flavor, ok := flavorsByName[flavorName]; ok {
return flavor, nil
}
return sdk.Flavor{}, fmt.Errorf("flavor %s not found in available flavors", flavorName)
}
// ReAuthenticate allows OpenStack keystone token to be revoked and re-created to call API
func (m *OvhCloudManager) ReAuthenticate() error {
if m.OpenStackProvider != nil {
if m.OpenStackProvider.IsTokenExpired() {
err := m.OpenStackProvider.ReauthenticateToken()
if err != nil {
return fmt.Errorf("failed to re-authenticate OpenStack token: %w", err)
}
client, err := sdk.NewDefaultClientWithToken(m.OpenStackProvider.AuthUrl, m.OpenStackProvider.Token)
if err != nil {
return fmt.Errorf("failed to re-create client: %w", err)
}
m.Client = client
}
}
return nil
}
// readConfig read cloud provider configuration file into a struct
func readConfig(configFile io.Reader) (*Config, error) {
cfg := &Config{}
if configFile != nil {
body, err := ioutil.ReadAll(configFile)
if err != nil {
return nil, fmt.Errorf("failed to read content: %w", err)
}
err = json.Unmarshal(body, cfg)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal body: %w", err)
}
}
return cfg, nil
}
// validatePayload check that cloud provider configuration file is correctly formatted
func validatePayload(cfg *Config) error {
if cfg.ClusterID == "" {
return fmt.Errorf("`cluster_id` not found in config file")
}
if cfg.ProjectID == "" {
return fmt.Errorf("`project_id` not found in config file")
}
if cfg.AuthenticationType != OpenStackAuthenticationType && cfg.AuthenticationType != ApplicationConsumerAuthenticationType {
return fmt.Errorf("`authentication_type` should only be `openstack` or `consumer`")
}
if cfg.AuthenticationType == OpenStackAuthenticationType {
if cfg.OpenStackAuthUrl == "" {
return fmt.Errorf("`openstack_auth_url` not found in config file")
}
if cfg.OpenStackUsername == "" {
return fmt.Errorf("`openstack_username` not found in config file")
}
if cfg.OpenStackPassword == "" {
return fmt.Errorf("`openstack_password` not found in config file")
}
if cfg.OpenStackDomain == "" {
return fmt.Errorf("`openstack_domain` not found in config file")
}
}
if cfg.AuthenticationType == ApplicationConsumerAuthenticationType {
if cfg.ApplicationEndpoint == "" {
return fmt.Errorf("`application_endpoint` not found in config file")
}
if cfg.ApplicationKey == "" {
return fmt.Errorf("`application_key` not found in config file")
}
if cfg.ApplicationSecret == "" {
return fmt.Errorf("`application_secret` not found in config file")
}
if cfg.ApplicationConsumerKey == "" {
return fmt.Errorf("`application_consumer_key` not found in config file")
}
}
return nil
}