Просмотр исходного кода

eks manual support implementation

Alexander Belanger 5 лет назад
Родитель
Сommit
c3e8cacd5d

+ 86 - 15
cli/cmd/connect/kubeconfig.go

@@ -107,7 +107,18 @@ func Kubeconfig(
 				}
 
 				resolvers = append(resolvers, resolveAction)
-			case models.AWSKeyDataAction:
+			case models.AWSDataAction:
+				resolveAction, err := resolveAWSAction(
+					saCandidate.ClusterEndpoint,
+					saCandidate.ClusterName,
+					saCandidate.AWSClusterIDGuess,
+				)
+
+				if err != nil {
+					return err
+				}
+
+				resolvers = append(resolvers, resolveAction)
 			}
 		}
 
@@ -126,20 +137,20 @@ func Kubeconfig(
 			color.New(color.FgGreen).Printf("created service account for cluster %s with id %d\n", cluster.Name, sa.ID)
 
 			// sanity check to ensure it's working
-			// namespaces, err := client.GetK8sNamespaces(
-			// 	context.Background(),
-			// 	projectID,
-			// 	sa.ID,
-			// 	cluster.ID,
-			// )
-
-			// if err != nil {
-			// 	return err
-			// }
-
-			// for _, ns := range namespaces.Items {
-			// 	fmt.Println(ns.ObjectMeta.GetName())
-			// }
+			namespaces, err := client.GetK8sNamespaces(
+				context.Background(),
+				projectID,
+				sa.ID,
+				cluster.ID,
+			)
+
+			if err != nil {
+				return err
+			}
+
+			for _, ns := range namespaces.Items {
+				fmt.Println(ns.ObjectMeta.GetName())
+			}
 		}
 	}
 
@@ -312,3 +323,63 @@ Key file location: `))
 }
 
 // resolves an aws key data action
+func resolveAWSAction(
+	endpoint string,
+	clusterName string,
+	awsClusterIDGuess string,
+) (*models.ServiceAccountAllActions, error) {
+	// just support manual for now
+	return resolveAWSActionManual(endpoint, clusterName, awsClusterIDGuess)
+}
+
+func resolveAWSActionManual(
+	endpoint string,
+	clusterName string,
+	awsClusterIDGuess string,
+) (*models.ServiceAccountAllActions, error) {
+	// query to see if the AWS cluster ID guess is correct
+	var clusterID string
+
+	userResp, err := utils.PromptPlaintext(
+		fmt.Sprintf(
+			`Detected AWS cluster ID as %s. Is this correct? %s `,
+			color.New(color.FgCyan).Sprintf(awsClusterIDGuess),
+			color.New(color.FgCyan).Sprintf("[y/n]"),
+		),
+	)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if userResp := strings.ToLower(userResp); userResp == "y" || userResp == "yes" {
+		clusterID = awsClusterIDGuess
+	} else {
+		clusterID, err = utils.PromptPlaintext(fmt.Sprintf(`Cluster ID: `))
+
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	// query for the access key id
+	accessKeyID, err := utils.PromptPlaintext(fmt.Sprintf(`AWS Access Key ID: `))
+
+	if err != nil {
+		return nil, err
+	}
+
+	// query for the secret access key
+	secretKey, err := utils.PromptPlaintext(fmt.Sprintf(`AWS Secret Access Key: `))
+
+	if err != nil {
+		return nil, err
+	}
+
+	return &models.ServiceAccountAllActions{
+		Name:               models.AWSDataAction,
+		AWSAccessKeyID:     accessKeyID,
+		AWSSecretAccessKey: secretKey,
+		AWSClusterID:       clusterID,
+	}, nil
+}

+ 1 - 0
cmd/migrate/main.go

@@ -29,6 +29,7 @@ func main() {
 		&models.ServiceAccountAction{},
 		&models.ServiceAccountCandidate{},
 		&models.Cluster{},
+		&models.TokenCache{},
 		&models.User{},
 		&models.Session{},
 		&models.RepoClient{},

+ 2 - 0
go.mod

@@ -7,6 +7,7 @@ require (
 	github.com/Azure/go-autorest/autorest v0.11.1 // indirect
 	github.com/DATA-DOG/go-sqlmock v1.5.0
 	github.com/Masterminds/semver v1.5.0 // indirect
+	github.com/aws/aws-sdk-go v1.30.0
 	github.com/containerd/containerd v1.4.1
 	github.com/cosmtrek/air v1.21.2 // indirect
 	github.com/creack/pty v1.1.11 // indirect
@@ -64,5 +65,6 @@ require (
 	k8s.io/helm v2.16.12+incompatible
 	k8s.io/klog/v2 v2.2.0 // indirect
 	k8s.io/utils v0.0.0-20200912215256-4140de9c8800 // indirect
+	sigs.k8s.io/aws-iam-authenticator v0.5.2
 	sigs.k8s.io/structured-merge-diff/v4 v4.0.1 // indirect
 )

+ 51 - 0
go.sum

@@ -114,6 +114,8 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:o
 github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
 github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.30.0 h1:7NDwnnQrI1Ivk0bXLzMmuX5ozzOwteHOsAs4druW7gI=
+github.com/aws/aws-sdk-go v1.30.0/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
 github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
 github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -186,6 +188,7 @@ github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrP
 github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4=
 github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
 github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
+github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -227,6 +230,7 @@ github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5m
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
 github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
+github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
@@ -348,12 +352,15 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
 github.com/godror/godror v0.13.3/go.mod h1:2ouUT4kdhUBk7TAkHWD4SN0CdI0pgEQbo8FVHhbSKWg=
+github.com/gofrs/flock v0.7.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+github.com/gofrs/flock v0.7.1 h1:DP+LD/t0njgoPBvT5MJLeliUIVQR03hiKR6vezdwHlc=
 github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
@@ -407,6 +414,7 @@ github.com/google/go-github/v32 v32.1.0 h1:GWkQOdXqviCPx7Q7Fj+KyPoGm4SwHRh8rheoP
 github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
 github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
+github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -563,12 +571,15 @@ github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
+github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
 github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
 github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
 github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd h1:nIzoSW6OhhppWLm4yqBwZsKJlAayUu5FGozhrF3ETSM=
 github.com/joeshaw/envdecode v0.0.0-20200121155833-099f1fc765bd/go.mod h1:MEQrHur0g8VplbLOv5vXmDzacSaH9Z7XhcgsSh1xciU=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
 github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
 github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -672,6 +683,7 @@ github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
 github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
@@ -698,10 +710,12 @@ github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2f
 github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
 github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
 github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
 github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@@ -744,15 +758,18 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA=
+github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
 github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
 github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
 github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc=
 github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og=
 github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
@@ -765,21 +782,26 @@ github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2
 github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
 github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY=
 github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA=
 github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
 github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
 github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
@@ -845,6 +867,7 @@ github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5J
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
@@ -882,6 +905,7 @@ github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wK
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
+go.hein.dev/go-version v0.1.0/go.mod h1:WOEm7DWMroRe5GdUgHMvx+Pji5WWIpMuXmK/3foylXs=
 go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.mongodb.org/mongo-driver v1.1.1/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
 go.mongodb.org/mongo-driver v1.1.2/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
@@ -931,7 +955,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
@@ -1050,6 +1076,8 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190812172437-4e8604ab3aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -1101,6 +1129,7 @@ golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGm
 golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
@@ -1115,6 +1144,7 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
 golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
 golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190812233024-afc3694995b6/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -1157,6 +1187,9 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0=
+gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
+gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ=
 google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
 google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
@@ -1302,23 +1335,29 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+k8s.io/api v0.16.8/go.mod h1:a8EOdYHO8en+YHhPBLiW5q+3RfHTr7wxTqqp7emJ7PM=
 k8s.io/api v0.18.8 h1:aIKUzJPb96f3fKec2lxtY7acZC9gQNDLVhfSGpxBAC4=
 k8s.io/api v0.18.8/go.mod h1:d/CXqwWv+Z2XEG1LgceeDmHQwpUJhROPx16SlxJgERY=
 k8s.io/apiextensions-apiserver v0.18.8 h1:pkqYPKTHa0/3lYwH7201RpF9eFm0lmZDFBNzhN+k/sA=
 k8s.io/apiextensions-apiserver v0.18.8/go.mod h1:7f4ySEkkvifIr4+BRrRWriKKIJjPyg9mb/p63dJKnlM=
+k8s.io/apimachinery v0.16.8/go.mod h1:Xk2vD2TRRpuWYLQNM6lT9R7DSFZUYG03SarNkbGrnKE=
 k8s.io/apimachinery v0.18.8 h1:jimPrycCqgx2QPearX3to1JePz7wSbVLq+7PdBTTwQ0=
 k8s.io/apimachinery v0.18.8/go.mod h1:6sQd+iHEqmOtALqOFjSWp2KZ9F0wlU/nWm0ZgsYWMig=
 k8s.io/apiserver v0.18.8/go.mod h1:12u5FuGql8Cc497ORNj79rhPdiXQC4bf53X/skR/1YM=
 k8s.io/cli-runtime v0.18.8 h1:ycmbN3hs7CfkJIYxJAOB10iW7BVPmXGXkfEyiV9NJ+k=
 k8s.io/cli-runtime v0.18.8/go.mod h1:7EzWiDbS9PFd0hamHHVoCY4GrokSTPSL32MA4rzIu0M=
+k8s.io/client-go v0.16.8/go.mod h1:WmPuN0yJTKHXoklExKxzo3jSXmr3EnN+65uaTb5VuNs=
 k8s.io/client-go v0.18.8 h1:SdbLpIxk5j5YbFr1b7fq8S7mDgDjYmUxSbszyoesoDM=
 k8s.io/client-go v0.18.8/go.mod h1:HqFqMllQ5NnQJNwjro9k5zMyfhZlOwpuTLVrxjkYSxU=
 k8s.io/client-go v1.5.1 h1:XaX/lo2/u3/pmFau8HN+sB5C/b4dc4Dmm2eXjBH4p1E=
 k8s.io/client-go v11.0.0+incompatible h1:LBbX2+lOwY9flffWlJM7f1Ct8V2SRNiMRDFeiwnJo9o=
+k8s.io/code-generator v0.16.8/go.mod h1:wFdrXdVi/UC+xIfLi+4l9elsTT/uEF61IfcN2wOLULQ=
 k8s.io/code-generator v0.18.8/go.mod h1:TgNEVx9hCyPGpdtCWA34olQYLkh3ok9ar7XfSsr8b6c=
+k8s.io/component-base v0.16.8/go.mod h1:Q8UWOWShpP3MZZny4n/15gOncfaaVtc9SbCdkM5MhUE=
 k8s.io/component-base v0.18.8 h1:BW5CORobxb6q5mb+YvdwQlyXXS6NVH5fDXWbU7tf2L8=
 k8s.io/component-base v0.18.8/go.mod h1:00frPRDas29rx58pPCxNkhUfPbwajlyyvu8ruNgSErU=
 k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
+k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
 k8s.io/helm v1.2.1 h1:Ny4wgW4p7X3tFXR34PziNkUxw2pV0G1DIFmI1QRDdo0=
@@ -1331,21 +1370,33 @@ k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
+k8s.io/kube-openapi v0.0.0-20190816220812-743ec37842bf/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6 h1:Oh3Mzx5pJ+yIumsAD0MOECPVeXsVot0UkiaCGVyfGQY=
 k8s.io/kube-openapi v0.0.0-20200410145947-61e04a5be9a6/go.mod h1:GRQhZsXIAJ1xR0C9bd8UpWHZ5plfAS9fzPjJuQ6JL3E=
 k8s.io/kubectl v0.18.8 h1:qTkHCz21YmK0+S0oE6TtjtxmjeDP42gJcZJyRKsIenA=
 k8s.io/kubectl v0.18.8/go.mod h1:PlEgIAjOMua4hDFTEkVf+W5M0asHUKfE4y7VDZkpLHM=
 k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
 k8s.io/metrics v0.18.8/go.mod h1:j7JzZdiyhLP2BsJm/Fzjs+j5Lb1Y7TySjhPWqBPwRXA=
+k8s.io/sample-controller v0.16.8/go.mod h1:aXlORS1ekU77qhGybB5t3JORDurzDpWgvMYxmCsiuos=
+k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200324210504-a9aa75ae1b89/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
 k8s.io/utils v0.0.0-20200912215256-4140de9c8800 h1:9ZNvfPvVIEsp/T1ez4GQuzCcCTEQWhovSofhqR73A6g=
 k8s.io/utils v0.0.0-20200912215256-4140de9c8800/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
+modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw=
+modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk=
+modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k=
+modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
+modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.7/go.mod h1:PHgbrJT7lCHcxMU+mDHEm+nx46H4zuuHZkDP6icnhu0=
+sigs.k8s.io/aws-iam-authenticator v0.5.2 h1:eGCtm6lLVpVVpsIBC1y4OwyQRhmg+A/OPXVMTlDKONc=
+sigs.k8s.io/aws-iam-authenticator v0.5.2/go.mod h1:yPDLi58MDx1UtCrRMOykLm1IyKKPGHgcGCafcbn2s3E=
 sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0=
 sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e h1:4Z09Hglb792X0kfOBBJUPFEyvVfQWrYT/l8h5EKA6JQ=
+sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0 h1:dOmIZBMfhcHS09XZkMyUgkq5trg3/jRyJYFZUiaOp8E=
 sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw=

+ 10 - 6
internal/forms/action.go

@@ -296,19 +296,21 @@ func (gkda *GCPKeyDataAction) PopulateServiceAccount(
 		return err
 	}
 
-	gkda.ServiceAccountActionResolver.SA.KeyData = []byte(gkda.GCPKeyData)
+	gkda.ServiceAccountActionResolver.SA.GCPKeyData = []byte(gkda.GCPKeyData)
 
 	return nil
 }
 
-// AWSKeyDataAction contains the AWS key data
-type AWSKeyDataAction struct {
+// AWSDataAction contains the AWS data (access id, key)
+type AWSDataAction struct {
 	*ServiceAccountActionResolver
-	AWSKeyData string `json:"aws_key_data" form:"required"`
+	AWSAccessKeyID     string `json:"aws_access_key_id" form:"required"`
+	AWSSecretAccessKey string `json:"aws_secret_access_key" form:"required"`
+	AWSClusterID       string `json:"aws_cluster_id" form:"required"`
 }
 
 // PopulateServiceAccount will add GCP key data to a ServiceAccount
-func (akda *AWSKeyDataAction) PopulateServiceAccount(
+func (akda *AWSDataAction) PopulateServiceAccount(
 	repo repository.ServiceAccountRepository,
 ) error {
 	err := akda.ServiceAccountActionResolver.PopulateServiceAccount(repo)
@@ -317,7 +319,9 @@ func (akda *AWSKeyDataAction) PopulateServiceAccount(
 		return err
 	}
 
-	akda.ServiceAccountActionResolver.SA.KeyData = []byte(akda.AWSKeyData)
+	akda.ServiceAccountActionResolver.SA.AWSAccessKeyID = akda.AWSAccessKeyID
+	akda.ServiceAccountActionResolver.SA.AWSSecretAccessKey = akda.AWSSecretAccessKey
+	akda.ServiceAccountActionResolver.SA.AWSClusterID = akda.AWSClusterID
 
 	return nil
 }

+ 19 - 8
internal/forms/action_test.go

@@ -382,16 +382,15 @@ func TestPopulateServiceAccountGCPKeyDataAction(t *testing.T) {
 		t.Errorf("service account auth mechanism is not %s\n", models.GCP)
 	}
 
-	if string(sa.KeyData) != string(gcpKeyData) {
+	if string(sa.GCPKeyData) != string(gcpKeyData) {
 		t.Errorf("service account token data is wrong: expected %s, got %s\n",
-			string(sa.KeyData), string(gcpKeyData))
+			string(sa.GCPKeyData), string(gcpKeyData))
 	}
 }
 
 func TestPopulateServiceAccountAWSKeyDataAction(t *testing.T) {
 	// create the in-memory repository
 	repo := test.NewRepository(true)
-	awsKeyData := []byte(`{"key": "data"}`)
 
 	// create a new project
 	repo.Project.CreateProject(&models.Project{
@@ -410,11 +409,13 @@ func TestPopulateServiceAccountAWSKeyDataAction(t *testing.T) {
 	}
 
 	// create a new form
-	form := forms.AWSKeyDataAction{
+	form := forms.AWSDataAction{
 		ServiceAccountActionResolver: &forms.ServiceAccountActionResolver{
 			ServiceAccountCandidateID: 1,
 		},
-		AWSKeyData: string(awsKeyData),
+		AWSAccessKeyID:     "ALSDKJFADSF",
+		AWSSecretAccessKey: "ASDLFKJALSDKFJ",
+		AWSClusterID:       "cluster-test",
 	}
 
 	err = form.PopulateServiceAccount(repo.ServiceAccount)
@@ -437,9 +438,19 @@ func TestPopulateServiceAccountAWSKeyDataAction(t *testing.T) {
 		t.Errorf("service account auth mechanism is not %s\n", models.AWS)
 	}
 
-	if string(sa.KeyData) != string(awsKeyData) {
-		t.Errorf("service account token data is wrong: expected %s, got %s\n",
-			string(sa.KeyData), string(awsKeyData))
+	if sa.AWSAccessKeyID != "ALSDKJFADSF" {
+		t.Errorf("service account aws access key id is wrong: expected %s, got %s\n",
+			"ALSDKJFADSF", sa.AWSAccessKeyID)
+	}
+
+	if sa.AWSSecretAccessKey != "ASDLFKJALSDKFJ" {
+		t.Errorf("service account aws access secret key is wrong: expected %s, got %s\n",
+			"ASDLFKJALSDKFJ", sa.AWSSecretAccessKey)
+	}
+
+	if sa.AWSClusterID != "cluster-test" {
+		t.Errorf("service account aws cluster id is wrong: expected %s, got %s\n",
+			"cluster-test", sa.AWSClusterID)
 	}
 }
 

+ 8 - 6
internal/helm/config.go

@@ -18,10 +18,11 @@ import (
 // Form represents the options for connecting to a cluster and
 // creating a Helm agent
 type Form struct {
-	ServiceAccount *models.ServiceAccount `form:"required"`
-	ClusterID      uint                   `json:"cluster_id" form:"required"`
-	Storage        string                 `json:"storage" form:"oneof=secret configmap memory"`
-	Namespace      string                 `json:"namespace"`
+	ServiceAccount   *models.ServiceAccount `form:"required"`
+	ClusterID        uint                   `json:"cluster_id" form:"required"`
+	Storage          string                 `json:"storage" form:"oneof=secret configmap memory"`
+	Namespace        string                 `json:"namespace"`
+	UpdateTokenCache kubernetes.UpdateTokenCacheFunc
 }
 
 // GetAgentOutOfClusterConfig creates a new Agent from outside the cluster using
@@ -29,8 +30,9 @@ type Form struct {
 func GetAgentOutOfClusterConfig(form *Form, l *logger.Logger) (*Agent, error) {
 	// create a kubernetes agent
 	conf := &kubernetes.OutOfClusterConfig{
-		ServiceAccount: form.ServiceAccount,
-		ClusterID:      form.ClusterID,
+		ServiceAccount:   form.ServiceAccount,
+		ClusterID:        form.ClusterID,
+		UpdateTokenCache: form.UpdateTokenCache,
 	}
 
 	k8sAgent, err := kubernetes.GetAgentOutOfClusterConfig(conf)

+ 8 - 2
internal/kubernetes/config.go

@@ -57,11 +57,16 @@ func GetAgentTesting(objects ...runtime.Object) *Agent {
 	return &Agent{&fakeRESTClientGetter{}, fake.NewSimpleClientset(objects...)}
 }
 
+// UpdateTokenCacheFunc is a function that updates the token cache
+// with a new token and expiry time
+type UpdateTokenCacheFunc func(token string, expiry time.Time) error
+
 // OutOfClusterConfig is the set of parameters required for an out-of-cluster connection.
 // This implements RESTClientGetter
 type OutOfClusterConfig struct {
-	ServiceAccount *models.ServiceAccount `form:"required"`
-	ClusterID      uint                   `json:"cluster_id" form:"required"`
+	ServiceAccount   *models.ServiceAccount `form:"required"`
+	ClusterID        uint                   `json:"cluster_id" form:"required"`
+	UpdateTokenCache UpdateTokenCacheFunc
 }
 
 // ToRESTConfig creates a kubernetes REST client factory -- it calls ClientConfig on
@@ -100,6 +105,7 @@ func (conf *OutOfClusterConfig) ToRawKubeConfigLoader() clientcmd.ClientConfig {
 	cmdConf, _ := GetClientConfigFromServiceAccount(
 		conf.ServiceAccount,
 		conf.ClusterID,
+		conf.UpdateTokenCache,
 	)
 
 	return cmdConf

+ 130 - 19
internal/kubernetes/kubeconfig.go

@@ -3,13 +3,18 @@ package kubernetes
 import (
 	"context"
 	"errors"
-	"fmt"
 	"strings"
 
 	"github.com/porter-dev/porter/internal/models"
 	"golang.org/x/oauth2/google"
 	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/clientcmd/api"
+
+	"github.com/aws/aws-sdk-go/aws"
+
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/session"
+	token "sigs.k8s.io/aws-iam-authenticator/pkg/token"
 )
 
 // GetServiceAccountCandidates parses a kubeconfig for a list of service account
@@ -31,6 +36,7 @@ func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCan
 
 	for contextName, context := range rawConf.Contexts {
 		clusterName := context.Cluster
+		awsClusterID := ""
 		authInfoName := context.AuthInfo
 
 		// get the auth mechanism and actions
@@ -42,6 +48,10 @@ func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCan
 		// if auth mechanism is unsupported, we'll skip it
 		if authMechanism == models.NotAvailable {
 			continue
+		} else if authMechanism == models.AWS {
+			// if the auth mechanism is AWS, we need to parse more explicitly
+			// for the cluster id
+			awsClusterID = parseAuthInfoForAWSClusterID(rawConf.AuthInfos[authInfoName], clusterName)
 		}
 
 		// construct the raw kubeconfig that's relevant for that context
@@ -56,12 +66,13 @@ func GetServiceAccountCandidates(kubeconfig []byte) ([]*models.ServiceAccountCan
 		if err == nil {
 			// create the candidate service account
 			res = append(res, &models.ServiceAccountCandidate{
-				Actions:         actions,
-				Kind:            "connector",
-				ClusterName:     clusterName,
-				ClusterEndpoint: rawConf.Clusters[clusterName].Server,
-				AuthMechanism:   authMechanism,
-				Kubeconfig:      rawBytes,
+				Actions:           actions,
+				Kind:              "connector",
+				ClusterName:       clusterName,
+				ClusterEndpoint:   rawConf.Clusters[clusterName].Server,
+				AuthMechanism:     authMechanism,
+				AWSClusterIDGuess: awsClusterID,
+				Kubeconfig:        rawBytes,
 			})
 		}
 	}
@@ -75,7 +86,6 @@ func GetRawConfigFromBytes(kubeconfig []byte) (*api.Config, error) {
 	config, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
 
 	if err != nil {
-		fmt.Println("ERROR IS HERE")
 		return nil, err
 	}
 
@@ -151,7 +161,7 @@ func parseAuthInfoForActions(authInfo *api.AuthInfo) (authMechanism string, acti
 		if authInfo.Exec.Command == "aws" || authInfo.Exec.Command == "aws-iam-authenticator" {
 			return models.AWS, []models.ServiceAccountAction{
 				models.ServiceAccountAction{
-					Name:     models.AWSKeyDataAction,
+					Name:     models.AWSDataAction,
 					Resolved: false,
 				},
 			}
@@ -197,6 +207,28 @@ func parseClusterForActions(cluster *api.Cluster) (actions []models.ServiceAccou
 	return actions
 }
 
+func parseAuthInfoForAWSClusterID(authInfo *api.AuthInfo, fallback string) string {
+	if authInfo.Exec != nil {
+		if authInfo.Exec.Command == "aws" {
+			// look for --cluster-name flag
+			for i, arg := range authInfo.Exec.Args {
+				if arg == "--cluster-name" && len(authInfo.Exec.Args) > i+1 {
+					return authInfo.Exec.Args[i+1]
+				}
+			}
+		} else if authInfo.Exec.Command == "aws-iam-authenticator" {
+			// look for -i or --cluster-id flag
+			for i, arg := range authInfo.Exec.Args {
+				if (arg == "-i" || arg == "--cluster-id") && len(authInfo.Exec.Args) > i+1 {
+					return authInfo.Exec.Args[i+1]
+				}
+			}
+		}
+	}
+
+	return fallback
+}
+
 // getKubeconfigForContext returns the raw kubeconfig associated with only a
 // single context of the raw config
 func getConfigForContext(
@@ -237,8 +269,9 @@ func getConfigForContext(
 func GetClientConfigFromServiceAccount(
 	sa *models.ServiceAccount,
 	clusterID uint,
+	updateTokenCache UpdateTokenCacheFunc,
 ) (clientcmd.ClientConfig, error) {
-	apiConfig, err := createRawConfigFromServiceAccount(sa, clusterID)
+	apiConfig, err := createRawConfigFromServiceAccount(sa, clusterID, updateTokenCache)
 
 	if err != nil {
 		return nil, err
@@ -252,6 +285,7 @@ func GetClientConfigFromServiceAccount(
 func createRawConfigFromServiceAccount(
 	sa *models.ServiceAccount,
 	clusterID uint,
+	updateTokenCache UpdateTokenCacheFunc,
 ) (*api.Config, error) {
 	apiConfig := &api.Config{}
 
@@ -313,22 +347,24 @@ func createRawConfigFromServiceAccount(
 				"refresh-token":                  sa.OIDCRefreshToken,
 			},
 		}
-	// we'll add a bearer token here for now
 	case models.GCP:
-		creds, err := google.CredentialsFromJSON(
-			context.Background(),
-			sa.KeyData,
-			"https://www.googleapis.com/auth/cloud-platform",
-		)
+		tok, err := getGCPToken(sa, updateTokenCache)
 
 		if err != nil {
 			return nil, err
 		}
 
-		tok, err := creds.TokenSource.Token()
-
-		authInfoMap[authInfoName].Token = tok.AccessToken
+		// add this as a bearer token
+		authInfoMap[authInfoName].Token = tok
 	case models.AWS:
+		tok, err := getAWSToken(sa, updateTokenCache)
+
+		if err != nil {
+			return nil, err
+		}
+
+		// add this as a bearer token
+		authInfoMap[authInfoName].Token = tok
 	default:
 		return nil, errors.New("not a supported auth mechanism")
 	}
@@ -350,6 +386,81 @@ func createRawConfigFromServiceAccount(
 	return apiConfig, nil
 }
 
+func getGCPToken(
+	sa *models.ServiceAccount,
+	updateTokenCache UpdateTokenCacheFunc,
+) (string, error) {
+	// check the token cache for a non-expired token
+	if tok := sa.TokenCache.Token; !sa.TokenCache.IsExpired() && tok != "" {
+		return tok, nil
+	}
+
+	creds, err := google.CredentialsFromJSON(
+		context.Background(),
+		sa.GCPKeyData,
+		"https://www.googleapis.com/auth/cloud-platform",
+	)
+
+	if err != nil {
+		return "", err
+	}
+
+	tok, err := creds.TokenSource.Token()
+
+	if err != nil {
+		return "", err
+	}
+
+	// update the token cache
+	updateTokenCache(tok.AccessToken, tok.Expiry)
+
+	return tok.AccessToken, nil
+}
+
+func getAWSToken(
+	sa *models.ServiceAccount,
+	updateTokenCache UpdateTokenCacheFunc,
+) (string, error) {
+	// check the token cache for a non-expired token
+	if tok := sa.TokenCache.Token; !sa.TokenCache.IsExpired() && tok != "" {
+		return tok, nil
+	}
+
+	generator, err := token.NewGenerator(false, false)
+
+	if err != nil {
+		return "", err
+	}
+
+	sess, err := session.NewSessionWithOptions(session.Options{
+		SharedConfigState: session.SharedConfigEnable,
+		Config: aws.Config{
+			Credentials: credentials.NewStaticCredentials(
+				sa.AWSAccessKeyID,
+				sa.AWSSecretAccessKey,
+				"",
+			),
+		},
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	tok, err := generator.GetWithOptions(&token.GetTokenOptions{
+		Session:   sess,
+		ClusterID: sa.AWSClusterID,
+	})
+
+	if err != nil {
+		return "", err
+	}
+
+	updateTokenCache(tok.Token, tok.Expiration)
+
+	return tok.Token, nil
+}
+
 // GetRestrictedClientConfigFromBytes returns a clientcmd.ClientConfig from a raw kubeconfig,
 // a context name, and the set of allowed contexts.
 func GetRestrictedClientConfigFromBytes(

+ 34 - 4
internal/kubernetes/kubeconfig_test.go

@@ -332,7 +332,7 @@ var SACandidatesTests = []saCandidatesTest{
 			&models.ServiceAccountCandidate{
 				Actions: []models.ServiceAccountAction{
 					models.ServiceAccountAction{
-						Name:     "upload-aws-key-data",
+						Name:     "upload-aws-data",
 						Resolved: false,
 					},
 				},
@@ -351,7 +351,7 @@ var SACandidatesTests = []saCandidatesTest{
 			&models.ServiceAccountCandidate{
 				Actions: []models.ServiceAccountAction{
 					models.ServiceAccountAction{
-						Name:     "upload-aws-key-data",
+						Name:     "upload-aws-data",
 						Resolved: false,
 					},
 				},
@@ -491,6 +491,36 @@ func TestGetServiceAccountCandidates(t *testing.T) {
 	}
 }
 
+func TestAWSClusterIDGuess(t *testing.T) {
+	result, err := kubernetes.GetServiceAccountCandidates([]byte(AWSIamAuthenticatorExec))
+
+	if err != nil {
+		t.Fatalf("error occurred %v\n", err)
+	}
+
+	if len(result) != 1 {
+		t.Fatalf("result length was not 1\n")
+	}
+
+	if result[0].AWSClusterIDGuess != "cluster-test-aws-id-guess" {
+		t.Errorf("Guess AWS cluster id failed: expected %s, got %s\n", "cluster-test-aws-id-guess", result[0].AWSClusterIDGuess)
+	}
+
+	result, err = kubernetes.GetServiceAccountCandidates([]byte(AWSEKSGetTokenExec))
+
+	if err != nil {
+		t.Fatalf("error occurred %v\n", err)
+	}
+
+	if len(result) != 1 {
+		t.Fatalf("result length was not 1\n")
+	}
+
+	if result[0].AWSClusterIDGuess != "cluster-test-aws-id-guess" {
+		t.Errorf("Guess AWS cluster id failed: expected %s, got %s\n", "cluster-test-aws-id-guess", result[0].AWSClusterIDGuess)
+	}
+}
+
 const noContexts string = `
 apiVersion: v1
 kind: Config
@@ -781,7 +811,7 @@ users:
       args:
         - "token"
         - "-i"
-        - "cluster-test"
+        - "cluster-test-aws-id-guess"
 `
 
 const AWSEKSGetTokenExec = `
@@ -809,7 +839,7 @@ users:
         - "eks"
         - "get-token"
         - "--cluster-name"
-        - "cluster-test"
+        - "cluster-test-aws-id-guess"
 `
 
 const OIDCAuthWithoutData = `

+ 13 - 11
internal/models/action.go

@@ -10,7 +10,7 @@ const (
 	OIDCIssuerDataAction        = "upload-oidc-idp-issuer-ca-data"
 	TokenDataAction             = "upload-token-data"
 	GCPKeyDataAction            = "upload-gcp-key-data"
-	AWSKeyDataAction            = "upload-aws-key-data"
+	AWSDataAction               = "upload-aws-data"
 )
 
 // ServiceAccountAction is an action that must be resolved to set up
@@ -58,13 +58,15 @@ type ServiceAccountActionExternal struct {
 type ServiceAccountAllActions struct {
 	Name string `json:"name"`
 
-	ClusterCAData    string `json:"cluster_ca_data,omitempty"`
-	ClientCertData   string `json:"client_cert_data,omitempty"`
-	ClientKeyData    string `json:"client_key_data,omitempty"`
-	OIDCIssuerCAData string `json:"oidc_idp_issuer_ca_data,omitempty"`
-	TokenData        string `json:"token_data,omitempty"`
-	GCPKeyData       string `json:"gcp_key_data,omitempty"`
-	AWSKeyData       string `json:"aws_key_data,omitempty"`
+	ClusterCAData      string `json:"cluster_ca_data,omitempty"`
+	ClientCertData     string `json:"client_cert_data,omitempty"`
+	ClientKeyData      string `json:"client_key_data,omitempty"`
+	OIDCIssuerCAData   string `json:"oidc_idp_issuer_ca_data,omitempty"`
+	TokenData          string `json:"token_data,omitempty"`
+	GCPKeyData         string `json:"gcp_key_data,omitempty"`
+	AWSAccessKeyID     string `json:"aws_access_key_id"`
+	AWSSecretAccessKey string `json:"aws_secret_access_key"`
+	AWSClusterID       string `json:"aws_cluster_id"`
 }
 
 // ServiceAccountActionInfo contains the information for actions to be
@@ -110,9 +112,9 @@ var ServiceAccountActionInfos = map[string]ServiceAccountActionInfo{
 		Docs:   "https://github.com/porter-dev/porter",
 		Fields: "gcp_key_data",
 	},
-	"upload-aws-key-data": ServiceAccountActionInfo{
-		Name:   AWSKeyDataAction,
+	"upload-aws-data": ServiceAccountActionInfo{
+		Name:   AWSDataAction,
 		Docs:   "https://github.com/porter-dev/porter",
-		Fields: "aws_key_data",
+		Fields: "aws_access_key_id,aws_secret_access_key,aws_cluster_id",
 	},
 }

+ 31 - 18
internal/models/serviceaccount.go

@@ -29,6 +29,10 @@ type ServiceAccountCandidate struct {
 	ClusterEndpoint string `json:"cluster_endpoint"`
 	AuthMechanism   string `json:"auth_mechanism"`
 
+	// The best-guess for the AWSClusterID, which is required by aws auth mechanisms
+	// See https://github.com/kubernetes-sigs/aws-iam-authenticator#what-is-a-cluster-id
+	AWSClusterIDGuess string `json:"aws_cluster_id_guess"`
+
 	// ------------------------------------------------------------------
 	// All fields below this line are encrypted before storage
 	// ------------------------------------------------------------------
@@ -39,13 +43,14 @@ type ServiceAccountCandidate struct {
 // ServiceAccountCandidateExternal represents the ServiceAccountCandidate type that is
 // sent over REST
 type ServiceAccountCandidateExternal struct {
-	ID              uint                           `json:"id"`
-	Actions         []ServiceAccountActionExternal `json:"actions"`
-	ProjectID       uint                           `json:"project_id"`
-	Kind            string                         `json:"kind"`
-	ClusterName     string                         `json:"cluster_name"`
-	ClusterEndpoint string                         `json:"cluster_endpoint"`
-	AuthMechanism   string                         `json:"auth_mechanism"`
+	ID                uint                           `json:"id"`
+	Actions           []ServiceAccountActionExternal `json:"actions"`
+	ProjectID         uint                           `json:"project_id"`
+	Kind              string                         `json:"kind"`
+	ClusterName       string                         `json:"cluster_name"`
+	ClusterEndpoint   string                         `json:"cluster_endpoint"`
+	AuthMechanism     string                         `json:"auth_mechanism"`
+	AWSClusterIDGuess string                         `json:"aws_cluster_id_guess"`
 }
 
 // Externalize generates an external ServiceAccountCandidate to be shared over REST
@@ -57,13 +62,14 @@ func (s *ServiceAccountCandidate) Externalize() *ServiceAccountCandidateExternal
 	}
 
 	return &ServiceAccountCandidateExternal{
-		ID:              s.ID,
-		Actions:         actions,
-		ProjectID:       s.ProjectID,
-		Kind:            s.Kind,
-		ClusterName:     s.ClusterName,
-		ClusterEndpoint: s.ClusterEndpoint,
-		AuthMechanism:   s.AuthMechanism,
+		ID:                s.ID,
+		Actions:           actions,
+		ProjectID:         s.ProjectID,
+		Kind:              s.Kind,
+		ClusterName:       s.ClusterName,
+		ClusterEndpoint:   s.ClusterEndpoint,
+		AuthMechanism:     s.AuthMechanism,
+		AWSClusterIDGuess: s.AWSClusterIDGuess,
 	}
 }
 
@@ -104,10 +110,17 @@ type ServiceAccount struct {
 	Username string `json:"username,omitempty"`
 	Password string `json:"password,omitempty"`
 
-	// KeyData for a service account for GCP and AWS connectors, along with
-	// a previous token so a new token isn't generated for each request
-	KeyData   []byte `json:"key_data"`
-	PrevToken string `json:"prev_token"`
+	// TokenCache is a cache for bearer tokens with an expiry time
+	// Used by GCP and AWS mechanisms
+	TokenCache TokenCache `json:"gcp_token_cache"`
+
+	// KeyData for a service account for GCP connectors
+	GCPKeyData []byte `json:"gcp_key_data"`
+
+	// AWS data
+	AWSAccessKeyID     string `json:"aws_access_key_id"`
+	AWSSecretAccessKey string `json:"aws_secret_access_key"`
+	AWSClusterID       string `json:"aws_cluster_id"`
 
 	// OIDC-related fields
 	OIDCIssuerURL                string `json:"idp-issuer-url"`

+ 28 - 0
internal/models/tokencache.go

@@ -0,0 +1,28 @@
+package models
+
+import (
+	"time"
+
+	"gorm.io/gorm"
+)
+
+// TokenCache stores a token and an expiration for the token for a
+// service account. This will never be shared over REST, so no need
+// to externalize.
+type TokenCache struct {
+	gorm.Model
+
+	ServiceAccountID uint      `json:"service_account_id"`
+	Expiry           time.Time `json:"expiry,omitempty"`
+
+	// ------------------------------------------------------------------
+	// All fields below this line are encrypted before storage
+	// ------------------------------------------------------------------
+
+	Token string `json:"access_token"`
+}
+
+// IsExpired returns true if a token is expired, false otherwise
+func (t *TokenCache) IsExpired() bool {
+	return time.Now().After(t.Expiry)
+}

+ 1 - 0
internal/repository/gorm/helpers_test.go

@@ -42,6 +42,7 @@ func setupTestEnv(tester *tester, t *testing.T) {
 		&models.ServiceAccountAction{},
 		&models.ServiceAccountCandidate{},
 		&models.Cluster{},
+		&models.TokenCache{},
 		&models.User{},
 		&models.Session{},
 		&models.RepoClient{},

+ 114 - 13
internal/repository/gorm/serviceaccount.go

@@ -107,6 +107,17 @@ func (repo *ServiceAccountRepository) CreateServiceAccount(
 		return nil, err
 	}
 
+	// create a token cache by default
+	assoc = repo.db.Model(sa).Association("TokenCache")
+
+	if assoc.Error != nil {
+		return nil, assoc.Error
+	}
+
+	if err := assoc.Append(&sa.TokenCache); err != nil {
+		return nil, err
+	}
+
 	return sa, nil
 }
 
@@ -117,7 +128,7 @@ func (repo *ServiceAccountRepository) ReadServiceAccount(
 	sa := &models.ServiceAccount{}
 
 	// preload Clusters association
-	if err := repo.db.Preload("Clusters").Where("id = ?", id).First(&sa).Error; err != nil {
+	if err := repo.db.Preload("Clusters").Preload("TokenCache").Where("id = ?", id).First(&sa).Error; err != nil {
 		return nil, err
 	}
 
@@ -144,6 +155,36 @@ func (repo *ServiceAccountRepository) ListServiceAccountsByProjectID(
 	return sas, nil
 }
 
+// UpdateServiceAccountTokenCache updates the token cache for a service account
+func (repo *ServiceAccountRepository) UpdateServiceAccountTokenCache(
+	tokenCache *models.TokenCache,
+) (*models.ServiceAccount, error) {
+	if tok := tokenCache.Token; tok != "" {
+		cipherData, err := repository.Encrypt([]byte(tok), repo.key)
+
+		if err != nil {
+			return nil, err
+		}
+
+		tokenCache.Token = string(cipherData)
+	}
+
+	sa := &models.ServiceAccount{}
+
+	if err := repo.db.Where("id = ?", tokenCache.ServiceAccountID).First(&sa).Error; err != nil {
+		return nil, err
+	}
+
+	sa.TokenCache.Token = tokenCache.Token
+	sa.TokenCache.Expiry = tokenCache.Expiry
+
+	if err := repo.db.Save(sa).Error; err != nil {
+		return nil, err
+	}
+
+	return sa, nil
+}
+
 // EncryptServiceAccountData will encrypt the user's service account data before writing
 // to the DB
 func (repo *ServiceAccountRepository) EncryptServiceAccountData(
@@ -200,24 +241,54 @@ func (repo *ServiceAccountRepository) EncryptServiceAccountData(
 		sa.Password = string(cipherData)
 	}
 
-	if len(sa.KeyData) > 0 {
-		cipherData, err := repository.Encrypt(sa.KeyData, key)
+	if len(sa.GCPKeyData) > 0 {
+		cipherData, err := repository.Encrypt(sa.GCPKeyData, key)
 
 		if err != nil {
 			return err
 		}
 
-		sa.KeyData = cipherData
+		sa.GCPKeyData = cipherData
 	}
 
-	if sa.PrevToken != "" {
-		cipherData, err := repository.Encrypt([]byte(sa.PrevToken), key)
+	if tok := sa.TokenCache.Token; tok != "" {
+		cipherData, err := repository.Encrypt([]byte(tok), key)
 
 		if err != nil {
 			return err
 		}
 
-		sa.PrevToken = string(cipherData)
+		sa.TokenCache.Token = string(cipherData)
+	}
+
+	if sa.AWSAccessKeyID != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.AWSAccessKeyID), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.AWSAccessKeyID = string(cipherData)
+	}
+
+	if sa.AWSSecretAccessKey != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.AWSSecretAccessKey), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.AWSSecretAccessKey = string(cipherData)
+	}
+
+	if sa.AWSClusterID != "" {
+		cipherData, err := repository.Encrypt([]byte(sa.AWSClusterID), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.AWSClusterID = string(cipherData)
 	}
 
 	if sa.OIDCIssuerURL != "" {
@@ -371,24 +442,54 @@ func (repo *ServiceAccountRepository) DecryptServiceAccountData(
 		sa.Password = string(plaintext)
 	}
 
-	if len(sa.KeyData) > 0 {
-		plaintext, err := repository.Decrypt(sa.KeyData, key)
+	if len(sa.GCPKeyData) > 0 {
+		plaintext, err := repository.Decrypt(sa.GCPKeyData, key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.GCPKeyData = plaintext
+	}
+
+	if tok := sa.TokenCache.Token; tok != "" {
+		plaintext, err := repository.Decrypt([]byte(tok), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.TokenCache.Token = string(plaintext)
+	}
+
+	if sa.AWSAccessKeyID != "" {
+		plaintext, err := repository.Decrypt([]byte(sa.AWSAccessKeyID), key)
+
+		if err != nil {
+			return err
+		}
+
+		sa.AWSAccessKeyID = string(plaintext)
+	}
+
+	if sa.AWSSecretAccessKey != "" {
+		plaintext, err := repository.Decrypt([]byte(sa.AWSSecretAccessKey), key)
 
 		if err != nil {
 			return err
 		}
 
-		sa.KeyData = plaintext
+		sa.AWSSecretAccessKey = string(plaintext)
 	}
 
-	if sa.PrevToken != "" {
-		plaintext, err := repository.Decrypt([]byte(sa.PrevToken), key)
+	if sa.AWSClusterID != "" {
+		plaintext, err := repository.Decrypt([]byte(sa.AWSClusterID), key)
 
 		if err != nil {
 			return err
 		}
 
-		sa.PrevToken = string(plaintext)
+		sa.AWSClusterID = string(plaintext)
 	}
 
 	if sa.OIDCIssuerURL != "" {

+ 74 - 0
internal/repository/gorm/serviceaccount_test.go

@@ -2,6 +2,7 @@ package gorm_test
 
 import (
 	"testing"
+	"time"
 
 	"github.com/go-test/deep"
 	"github.com/porter-dev/porter/internal/models"
@@ -342,3 +343,76 @@ func TestListServiceAccountsByProjectID(t *testing.T) {
 		t.Error(diff)
 	}
 }
+
+func TestUpdateServiceAccountToken(t *testing.T) {
+	tester := &tester{
+		dbFileName: "./porter_test_update_sa_token.db",
+	}
+
+	setupTestEnv(tester, t)
+	initProject(tester, t)
+	defer cleanup(tester, t)
+
+	sa := &models.ServiceAccount{
+		ProjectID:     1,
+		Kind:          "connector",
+		AuthMechanism: models.GCP,
+		GCPKeyData:    []byte(`{"key":"data"}`),
+		TokenCache: models.TokenCache{
+			Token:  "token-1",
+			Expiry: time.Now().Add(-1 * time.Hour),
+		},
+	}
+
+	sa, err := tester.repo.ServiceAccount.CreateServiceAccount(sa)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
+
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure service account id of token is 1
+	if sa.TokenCache.ServiceAccountID != 1 {
+		t.Fatalf("incorrect service account ID in token cache: expected %d, got %d\n", 1, sa.TokenCache.ServiceAccountID)
+	}
+
+	// make sure old token is expired
+	if isExpired := sa.TokenCache.IsExpired(); !isExpired {
+		t.Fatalf("token was not expired\n")
+	}
+
+	sa.TokenCache.Token = "token-2"
+	sa.TokenCache.Expiry = time.Now().Add(24 * time.Hour)
+
+	sa, err = tester.repo.ServiceAccount.UpdateServiceAccountTokenCache(&sa.TokenCache)
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+	sa, err = tester.repo.ServiceAccount.ReadServiceAccount(sa.Model.ID)
+	if err != nil {
+		t.Fatalf("%v\n", err)
+	}
+
+	// make sure id is 1
+	if sa.Model.ID != 1 {
+		t.Errorf("incorrect service account ID: expected %d, got %d\n", 1, sa.Model.ID)
+	}
+
+	// make sure new token is correct and not expired
+	if sa.TokenCache.ServiceAccountID != 1 {
+		t.Fatalf("incorrect service account ID in token cache: expected %d, got %d\n", 1, sa.TokenCache.ServiceAccountID)
+	}
+
+	if isExpired := sa.TokenCache.IsExpired(); isExpired {
+		t.Fatalf("token was expired\n")
+	}
+
+	if sa.TokenCache.Token != "token-2" {
+		t.Errorf("incorrect token in cache: expected %s, got %s\n", "token-2", sa.TokenCache.Token)
+	}
+}

+ 1 - 0
internal/repository/serviceaccount.go

@@ -13,4 +13,5 @@ type ServiceAccountRepository interface {
 	CreateServiceAccount(sa *models.ServiceAccount) (*models.ServiceAccount, error)
 	ReadServiceAccount(id uint) (*models.ServiceAccount, error)
 	ListServiceAccountsByProjectID(projectID uint) ([]*models.ServiceAccount, error)
+	UpdateServiceAccountTokenCache(tokenCache *models.TokenCache) (*models.ServiceAccount, error)
 }

+ 15 - 0
internal/repository/test/serviceaccount.go

@@ -152,3 +152,18 @@ func (repo *ServiceAccountRepository) createCluster(
 
 	return cluster, nil
 }
+
+// UpdateServiceAccountTokenCache updates the token cache for a service account
+func (repo *ServiceAccountRepository) UpdateServiceAccountTokenCache(
+	tokenCache *models.TokenCache,
+) (*models.ServiceAccount, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	index := int(tokenCache.ServiceAccountID - 1)
+	repo.serviceAccounts[index].TokenCache.Token = tokenCache.Token
+	repo.serviceAccounts[index].TokenCache.Expiry = tokenCache.Expiry
+
+	return repo.serviceAccounts[index], nil
+}

+ 16 - 1
server/api/k8s_handler.go

@@ -4,8 +4,10 @@ import (
 	"encoding/json"
 	"net/http"
 	"net/url"
+	"time"
 
 	"github.com/porter-dev/porter/internal/kubernetes"
+	"github.com/porter-dev/porter/internal/models"
 
 	"github.com/porter-dev/porter/internal/forms"
 )
@@ -27,7 +29,9 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 
 	// get the filter options
 	form := &forms.K8sForm{
-		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{},
+		OutOfClusterConfig: &kubernetes.OutOfClusterConfig{
+			UpdateTokenCache: app.updateTokenCache,
+		},
 	}
 
 	form.PopulateK8sOptionsFromQueryParams(vals, app.repo.ServiceAccount)
@@ -59,3 +63,14 @@ func (app *App) HandleListNamespaces(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 }
+
+func (app *App) updateTokenCache(token string, expiry time.Time) error {
+	_, err := app.repo.ServiceAccount.UpdateServiceAccountTokenCache(
+		&models.TokenCache{
+			Token:  token,
+			Expiry: expiry,
+		},
+	)
+
+	return err
+}

+ 5 - 3
server/api/project_handler.go

@@ -350,10 +350,12 @@ func (app *App) HandleResolveSACandidateActions(w http.ResponseWriter, r *http.R
 			}
 
 			err = form.PopulateServiceAccount(app.repo.ServiceAccount)
-		case models.AWSKeyDataAction:
-			form := &forms.AWSKeyDataAction{
+		case models.AWSDataAction:
+			form := &forms.AWSDataAction{
 				ServiceAccountActionResolver: saResolverBase,
-				AWSKeyData:                   action.AWSKeyData,
+				AWSAccessKeyID:               action.AWSAccessKeyID,
+				AWSSecretAccessKey:           action.AWSSecretAccessKey,
+				AWSClusterID:                 action.AWSClusterID,
 			}
 
 			err = form.PopulateServiceAccount(app.repo.ServiceAccount)

+ 18 - 6
server/api/release_handler.go

@@ -26,7 +26,9 @@ const (
 func (app *App) HandleListReleases(w http.ResponseWriter, r *http.Request) {
 	form := &forms.ListReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
-			Form: &helm.Form{},
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
 		},
 		ListFilter: &helm.ListFilter{},
 	}
@@ -64,7 +66,9 @@ func (app *App) HandleGetRelease(w http.ResponseWriter, r *http.Request) {
 
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
-			Form: &helm.Form{},
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
 		},
 		Name:     name,
 		Revision: int(revision),
@@ -106,7 +110,9 @@ func (app *App) HandleGetReleaseComponents(w http.ResponseWriter, r *http.Reques
 
 	form := &forms.GetReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
-			Form: &helm.Form{},
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
 		},
 		Name:     name,
 		Revision: int(revision),
@@ -158,7 +164,9 @@ func (app *App) HandleListReleaseHistory(w http.ResponseWriter, r *http.Request)
 
 	form := &forms.ListReleaseHistoryForm{
 		ReleaseForm: &forms.ReleaseForm{
-			Form: &helm.Form{},
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
 		},
 		Name: name,
 	}
@@ -205,7 +213,9 @@ func (app *App) HandleUpgradeRelease(w http.ResponseWriter, r *http.Request) {
 
 	form := &forms.UpgradeReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
-			Form: &helm.Form{},
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
 		},
 		Name: name,
 	}
@@ -258,7 +268,9 @@ func (app *App) HandleRollbackRelease(w http.ResponseWriter, r *http.Request) {
 
 	form := &forms.RollbackReleaseForm{
 		ReleaseForm: &forms.ReleaseForm{
-			Form: &helm.Form{},
+			Form: &helm.Form{
+				UpdateTokenCache: app.updateTokenCache,
+			},
 		},
 		Name: name,
 	}