Parcourir la source

Accept a service-key.json via Kubernetes secret that represents the service account key. Prioritize config based keys over secret.

Matt Bolt il y a 6 ans
Parent
commit
ad0d2c6127

+ 56 - 13
pkg/cloud/awsprovider.go

@@ -9,7 +9,6 @@ import (
 	"io"
 	"io/ioutil"
 	"net/http"
-	"net/url"
 	"os"
 	"regexp"
 	"strconv"
@@ -20,6 +19,7 @@ import (
 	"k8s.io/klog"
 
 	"github.com/kubecost/cost-model/pkg/clustercache"
+	"github.com/kubecost/cost-model/pkg/util"
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/awserr"
@@ -67,6 +67,11 @@ type AWS struct {
 	*CustomProvider
 }
 
+type AWSAccessKey struct {
+	AccessKeyID     string `json:"aws_access_key_id"`
+	SecretAccessKey string `json:"aws_secret_access_key"`
+}
+
 // AWSPricing maps a k8s node to an AWS Pricing "product"
 type AWSPricing struct {
 	Products map[string]*AWSProduct `json:"products"`
@@ -209,6 +214,9 @@ var regionToBillingRegionCode = map[string]string{
 	"us-gov-west-1":  "UGW1",
 }
 
+var loadedAWSSecret bool = false
+var awsSecret *AWSAccessKey = nil
+
 func (aws *AWS) GetLocalStorageQuery(window, offset string, rate bool, used bool) string {
 	return ""
 }
@@ -269,6 +277,7 @@ func (aws *AWS) GetConfig() (*CustomPricing, error) {
 	if err != nil {
 		return nil, err
 	}
+
 	return c, nil
 }
 func (aws *AWS) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
@@ -474,8 +483,10 @@ func (aws *AWS) DownloadPricingData() error {
 	aws.SpotDataPrefix = c.SpotDataPrefix
 	aws.ProjectID = c.ProjectID
 	aws.SpotDataRegion = c.SpotDataRegion
-	aws.ServiceKeyName = c.ServiceKeyName
-	aws.ServiceKeySecret = c.ServiceKeySecret
+
+	skn, sks := aws.getAWSAuth(false, c)
+	aws.ServiceKeyName = skn
+	aws.ServiceKeySecret = sks
 
 	if len(aws.SpotDataBucket) != 0 && len(aws.ProjectID) == 0 {
 		klog.V(1).Infof("using SpotDataBucket \"%s\" without ProjectID will not end well", aws.SpotDataBucket)
@@ -939,18 +950,50 @@ func (awsProvider *AWS) ClusterInfo() (map[string]string, error) {
 	return makeStructure(defaultClusterName)
 }
 
-// AddServiceKey adds an AWS service key, useful for pulling down out-of-cluster costs. Optional-- the container this runs in can be directly authorized.
-func (*AWS) AddServiceKey(formValues url.Values) error {
-	keyID := formValues.Get("access_key_ID")
-	key := formValues.Get("secret_access_key")
-	m := make(map[string]string)
-	m["access_key_ID"] = keyID
-	m["secret_access_key"] = key
-	result, err := json.Marshal(m)
+// Gets the aws key id and secret
+func (aws *AWS) getAWSAuth(forceReload bool, cp *CustomPricing) (string, string) {
+	// 1. Check config values first (set from frontend UI)
+	if cp.ServiceKeyName != "" && cp.ServiceKeySecret != "" {
+		return cp.ServiceKeyName, cp.ServiceKeySecret
+	}
+
+	// 2. Check for secret
+	s, _ := aws.loadAWSAuthSecret(forceReload)
+	if s != nil && s.AccessKeyID != "" && s.SecretAccessKey != "" {
+		return s.AccessKeyID, s.SecretAccessKey
+	}
+
+	// 3. Fall back to env vars
+	return os.Getenv(awsAccessKeyIDEnvVar), os.Getenv(awsAccessKeySecretEnvVar)
+}
+
+// Load once and cache the result (even on failure). This is an install time secret, so
+// we don't expect the secret to change. If it does, however, we can force reload using
+// the input parameter.
+func (aws *AWS) loadAWSAuthSecret(force bool) (*AWSAccessKey, error) {
+	if !force && loadedAWSSecret {
+		return awsSecret, nil
+	}
+	loadedAWSSecret = true
+
+	exists, err := util.FileExists(authSecretPath)
+	if !exists || err != nil {
+		return nil, fmt.Errorf("Failed to locate service account file: %s", authSecretPath)
+	}
+
+	result, err := ioutil.ReadFile(authSecretPath)
 	if err != nil {
-		return err
+		return nil, err
 	}
-	return ioutil.WriteFile("/var/configs/key.json", result, 0644)
+
+	var ak AWSAccessKey
+	err = json.Unmarshal(result, &ak)
+	if err != nil {
+		return nil, err
+	}
+
+	awsSecret = &ak
+	return awsSecret, nil
 }
 
 func (aws *AWS) configureAWSAuth() error {

+ 90 - 6
pkg/cloud/azureprovider.go

@@ -5,7 +5,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"net/url"
+	"io/ioutil"
 	"os"
 	"regexp"
 	"strconv"
@@ -13,6 +13,7 @@ import (
 	"sync"
 
 	"github.com/kubecost/cost-model/pkg/clustercache"
+	"github.com/kubecost/cost-model/pkg/util"
 
 	"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2017-09-01/skus"
 	"github.com/Azure/azure-sdk-for-go/services/containerservice/mgmt/2018-03-31/containerservice"
@@ -59,6 +60,9 @@ var (
 	mtStandardN, _ = regexp.Compile(`^Standard_N[C|D|V]\d+r?[_v\d]*[_Promo]*$`)
 )
 
+var loadedAzureSecret bool = false
+var azureSecret *AzureServiceKey = nil
+
 type regionParts []string
 
 func (r regionParts) String() string {
@@ -197,13 +201,90 @@ func (k *azureKey) GPUType() string {
 		return t
 	}
 	return ""
-
 }
 
 func (k *azureKey) ID() string {
 	return ""
 }
 
+// Represents an azure app key
+type AzureAppKey struct {
+	AppID       string `json:"appId"`
+	DisplayName string `json:"displayName"`
+	Name        string `json:"name"`
+	Password    string `json:"password"`
+	Tenant      string `json:"tenant"`
+}
+
+// Azure service key for a specific subscription
+type AzureServiceKey struct {
+	SubscriptionID string       `json:"subscriptionId"`
+	ServiceKey     *AzureAppKey `json:"serviceKey"`
+}
+
+// Validity check on service key
+func (ask *AzureServiceKey) IsValid() bool {
+	return ask.SubscriptionID != "" &&
+		ask.ServiceKey != nil &&
+		ask.ServiceKey.AppID != "" &&
+		ask.ServiceKey.Password != "" &&
+		ask.ServiceKey.Tenant != ""
+}
+
+// Loads the azure authentication via configuration or a secret set at install time.
+func (az *Azure) getAzureAuth(forceReload bool, cp *CustomPricing) (subscriptionID, clientID, clientSecret, tenantID string) {
+	// 1. Check config values first (set from frontend UI)
+	if cp.AzureSubscriptionID != "" && cp.AzureClientID != "" && cp.AzureClientSecret != "" && cp.AzureTenantID != "" {
+		subscriptionID = cp.AzureSubscriptionID
+		clientID = cp.AzureClientID
+		clientSecret = cp.AzureClientSecret
+		tenantID = cp.AzureTenantID
+		return
+	}
+
+	// 2. Check for secret
+	s, _ := az.loadAzureAuthSecret(forceReload)
+	if s != nil && s.IsValid() {
+		subscriptionID = s.SubscriptionID
+		clientID = s.ServiceKey.AppID
+		clientSecret = s.ServiceKey.Password
+		tenantID = s.ServiceKey.Tenant
+		return
+	}
+
+	// 3. Empty values
+	return "", "", "", ""
+}
+
+// Load once and cache the result (even on failure). This is an install time secret, so
+// we don't expect the secret to change. If it does, however, we can force reload using
+// the input parameter.
+func (az *Azure) loadAzureAuthSecret(force bool) (*AzureServiceKey, error) {
+	if !force && loadedAzureSecret {
+		return azureSecret, nil
+	}
+	loadedAzureSecret = true
+
+	exists, err := util.FileExists(authSecretPath)
+	if !exists || err != nil {
+		return nil, fmt.Errorf("Failed to locate service account file: %s", authSecretPath)
+	}
+
+	result, err := ioutil.ReadFile(authSecretPath)
+	if err != nil {
+		return nil, err
+	}
+
+	var ask AzureServiceKey
+	err = json.Unmarshal(result, &ask)
+	if err != nil {
+		return nil, err
+	}
+
+	azureSecret = &ask
+	return azureSecret, nil
+}
+
 func (az *Azure) GetKey(labels map[string]string) Key {
 	cfg, err := az.GetConfig()
 	if err != nil {
@@ -319,6 +400,13 @@ func (az *Azure) DownloadPricingData() error {
 		return err
 	}
 
+	// Load the service provider keys
+	subscriptionID, clientID, clientSecret, tenantID := az.getAzureAuth(false, config)
+	config.AzureSubscriptionID = subscriptionID
+	config.AzureClientID = clientID
+	config.AzureClientSecret = clientSecret
+	config.AzureTenantID = tenantID
+
 	var authorizer autorest.Authorizer
 
 	if config.AzureClientID != "" && config.AzureClientSecret != "" && config.AzureTenantID != "" {
@@ -602,10 +690,6 @@ func (az *Azure) ClusterInfo() (map[string]string, error) {
 
 }
 
-func (az *Azure) AddServiceKey(url url.Values) error {
-	return nil
-}
-
 func (az *Azure) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
 	return az.Config.UpdateFromMap(a)
 }

+ 0 - 5
pkg/cloud/customprovider.go

@@ -3,7 +3,6 @@ package cloud
 import (
 	"encoding/json"
 	"io"
-	"net/url"
 	"strconv"
 	"strings"
 	"sync"
@@ -109,10 +108,6 @@ func (cp *CustomProvider) ClusterInfo() (map[string]string, error) {
 	return m, nil
 }
 
-func (*CustomProvider) AddServiceKey(url.Values) error {
-	return nil
-}
-
 func (*CustomProvider) GetDisks() ([]byte, error) {
 	return nil, nil
 }

+ 49 - 20
pkg/cloud/gcpprovider.go

@@ -8,7 +8,6 @@ import (
 	"io/ioutil"
 	"math"
 	"net/http"
-	"net/url"
 	"os"
 	"regexp"
 	"strconv"
@@ -21,6 +20,7 @@ import (
 	"cloud.google.com/go/bigquery"
 	"cloud.google.com/go/compute/metadata"
 	"github.com/kubecost/cost-model/pkg/clustercache"
+	"github.com/kubecost/cost-model/pkg/util"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2/google"
 	compute "google.golang.org/api/compute/v1"
@@ -181,6 +181,38 @@ func (gcp *GCP) GetManagementPlatform() (string, error) {
 	return "", nil
 }
 
+// Attempts to load a GCP auth secret and copy the contents to the key file.
+func (*GCP) loadGCPAuthSecret() {
+	path := os.Getenv("CONFIG_PATH")
+	if path == "" {
+		path = "/models/"
+	}
+
+	keyPath := path + "key.json"
+	keyExists, _ := util.FileExists(keyPath)
+	if keyExists {
+		klog.V(1).Infof("GCP Auth Key already exists, no need to load from secret")
+		return
+	}
+
+	exists, err := util.FileExists(authSecretPath)
+	if !exists || err != nil {
+		klog.V(4).Infof("[Warning] Failed to load auth secret, or was not mounted: %s", err.Error())
+		return
+	}
+
+	result, err := ioutil.ReadFile(authSecretPath)
+	if err != nil {
+		klog.V(4).Infof("[Warning] Failed to load auth secret, or was not mounted: %s", err.Error())
+		return
+	}
+
+	err = ioutil.WriteFile(keyPath, result, 0644)
+	if err != nil {
+		klog.V(4).Infof("[Warning] Failed to copy auth secret to %s: %s", keyPath, err.Error())
+	}
+}
+
 func (gcp *GCP) UpdateConfigFromConfigMap(a map[string]string) (*CustomPricing, error) {
 	return gcp.Config.UpdateFromMap(a)
 }
@@ -197,20 +229,22 @@ func (gcp *GCP) UpdateConfig(r io.Reader, updateType string) (*CustomPricing, er
 			c.ProjectID = a.ProjectID
 			c.BillingDataDataset = a.BillingDataDataset
 
-			j, err := json.Marshal(a.Key)
-			if err != nil {
-				return err
-			}
+			if len(a.Key) > 0 {
+				j, err := json.Marshal(a.Key)
+				if err != nil {
+					return err
+				}
 
-			path := os.Getenv("CONFIG_PATH")
-			if path == "" {
-				path = "/models/"
-			}
+				path := os.Getenv("CONFIG_PATH")
+				if path == "" {
+					path = "/models/"
+				}
 
-			keyPath := path + "key.json"
-			err = ioutil.WriteFile(keyPath, j, 0644)
-			if err != nil {
-				return err
+				keyPath := path + "key.json"
+				err = ioutil.WriteFile(keyPath, j, 0644)
+				if err != nil {
+					return err
+				}
 			}
 		} else if updateType == AthenaInfoUpdateType {
 			a := AwsAthenaInfo{}
@@ -444,13 +478,6 @@ func (gcp *GCP) ClusterInfo() (map[string]string, error) {
 	return m, nil
 }
 
-// AddServiceKey adds the service key as required for GetDisks
-func (*GCP) AddServiceKey(formValues url.Values) error {
-	key := formValues.Get("key")
-	k := []byte(key)
-	return ioutil.WriteFile("/var/configs/key.json", k, 0644)
-}
-
 // GetDisks returns the GCP disks backing PVs. Useful because sometimes k8s will not clean up PVs correctly. Requires a json config in /var/configs with key region.
 func (*GCP) GetDisks() ([]byte, error) {
 	// metadata API setup
@@ -858,6 +885,8 @@ func (gcp *GCP) DownloadPricingData() error {
 		klog.V(2).Infof("Error downloading default pricing data: %s", err.Error())
 		return err
 	}
+	gcp.loadGCPAuthSecret()
+
 	gcp.BaseCPUPrice = c.CPU
 	gcp.ProjectID = c.ProjectID
 	gcp.BillingDataDataset = c.BillingDataDataset

+ 1 - 2
pkg/cloud/provider.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"net/url"
 	"os"
 	"strings"
 
@@ -21,6 +20,7 @@ const clusterIDKey = "CLUSTER_ID"
 const remoteEnabled = "REMOTE_WRITE_ENABLED"
 const remotePW = "REMOTE_WRITE_PASSWORD"
 const sqlAddress = "SQL_ADDRESS"
+const authSecretPath = "/var/secrets/service-key.json"
 
 var createTableStatements = []string{
 	`CREATE TABLE IF NOT EXISTS names (
@@ -162,7 +162,6 @@ type CustomPricing struct {
 // Provider represents a k8s provider.
 type Provider interface {
 	ClusterInfo() (map[string]string, error)
-	AddServiceKey(url.Values) error
 	GetDisks() ([]byte, error)
 	NodePricing(Key) (*Node, error)
 	PVPricing(PVKey) (*PV, error)