Răsfoiți Sursa

changes to support cluster control plane capi cluster

Stefan McShane 3 ani în urmă
părinte
comite
a3a0695d1d

+ 1 - 0
.gitignore

@@ -16,6 +16,7 @@ staging.sh
 bin
 openapi.yaml
 .idea
+vendor
 
 # Local docs directories
 /docs/.obsidian

+ 5 - 4
api/server/authz/cluster.go

@@ -86,10 +86,11 @@ func NewOutOfClusterAgentGetter(config *config.Config) KubernetesAgentGetter {
 
 func (d *OutOfClusterAgentGetter) GetOutOfClusterConfig(cluster *models.Cluster) *kubernetes.OutOfClusterConfig {
 	return &kubernetes.OutOfClusterConfig{
-		Repo:                      d.config.Repo,
-		DigitalOceanOAuth:         d.config.DOConf,
-		Cluster:                   cluster,
-		AllowInClusterConnections: d.config.ServerConf.InitInCluster,
+		Repo:                        d.config.Repo,
+		DigitalOceanOAuth:           d.config.DOConf,
+		Cluster:                     cluster,
+		AllowInClusterConnections:   d.config.ServerConf.InitInCluster,
+		CAPIManagementClusterClient: d.config.ClusterControlPlaneClient,
 	}
 }
 

+ 29 - 1
api/server/handlers/cluster/get_kubeconfig.go

@@ -1,9 +1,13 @@
 package cluster
 
 import (
+	"context"
 	"errors"
+	"fmt"
 	"net/http"
 
+	"github.com/bufbuild/connect-go"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
@@ -36,11 +40,35 @@ func (c *GetTemporaryKubeconfigHandler) ServeHTTP(w http.ResponseWriter, r *http
 		))
 		return
 	}
+	ctx := r.Context()
 
-	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster)
 
 	outOfClusterConfig := c.GetOutOfClusterConfig(cluster)
 
+	if cluster.ProvisionedBy == "CAPI" {
+		kubeconfigResp, err := c.Config().ClusterControlPlaneClient.KubeConfigForCluster(context.Background(), connect.NewRequest(
+			&porterv1.KubeConfigForClusterRequest{
+				ProjectId: int64(cluster.ProjectID),
+				ClusterId: int64(cluster.ID),
+			},
+		))
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting temporary capi config: %w", err)))
+			return
+		}
+
+		if kubeconfigResp.Msg == nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error reading temporary capi config: %w", err)))
+			return
+		}
+		res := &types.GetTemporaryKubeconfigResponse{
+			Kubeconfig: []byte(kubeconfigResp.Msg.KubeConfig),
+		}
+		c.WriteResult(w, r, res)
+		return
+	}
+
 	kubeconfig, err := outOfClusterConfig.CreateRawConfigFromCluster()
 
 	if err != nil {

+ 5 - 5
api/server/handlers/release/create.go

@@ -63,7 +63,7 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	helmAgent, err := c.GetHelmAgent(r, cluster, "")
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting helm agent: %w", err)))
 		return
 	}
 
@@ -83,7 +83,7 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 		hrs, err := c.Repo().HelmRepo().ListHelmReposByProjectID(cluster.ProjectID)
 
 		if err != nil {
-			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing helm repos for project : %w", err)))
 			return
 		}
 
@@ -106,14 +106,14 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	chart, err := loader.LoadChartPublic(request.RepoURL, request.TemplateName, request.TemplateVersion)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error loading public chart: %w", err)))
 		return
 	}
 
 	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error listing registries: %w", err)))
 		return
 	}
 
@@ -141,7 +141,7 @@ func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)
 	k8sAgent, err := c.GetAgent(r, cluster, "")
 
 	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("error getting k8s agent: %w", err)))
 		return
 	}
 

+ 4 - 0
api/server/shared/config/config.go

@@ -2,6 +2,7 @@ package config
 
 import (
 	"github.com/gorilla/sessions"
+	"github.com/porter-dev/api-contracts/generated/go/porter/v1/porterv1connect"
 	"github.com/porter-dev/porter/api/server/shared/apierrors/alerter"
 	"github.com/porter-dev/porter/api/server/shared/config/env"
 	"github.com/porter-dev/porter/api/server/shared/websocket"
@@ -93,6 +94,9 @@ type Config struct {
 	// PowerDNSClient is a client for PowerDNS, if the Porter instance supports vanity URLs
 	PowerDNSClient *powerdns.Client
 
+	// ClusterControlPlaneClient is a client for ClusterControlPlane
+	ClusterControlPlaneClient porterv1connect.ClusterControlPlaneServiceClient
+
 	// CredentialBackend is the backend for credential storage, if external cred storage (like Vault)
 	// is used
 	CredentialBackend credentials.CredentialStorage

+ 3 - 0
api/server/shared/config/env/envconfs.go

@@ -76,6 +76,9 @@ type ServerConf struct {
 	ProvisionerServerURL string `env:"PROVISIONER_SERVER_URL"`
 	ProvisionerToken     string `env:"PROVISIONER_TOKEN"`
 
+	// ClusterControlPlane settings
+	ClusterControlPlaneAddress string `env:"CLUSTER_CONTROL_PLANE_ADDRESS"`
+
 	SegmentClientKey string `env:"SEGMENT_CLIENT_KEY"`
 
 	// PowerDNS client API key and the host of the PowerDNS API server

+ 8 - 0
api/server/shared/config/loader/loader.go

@@ -1,12 +1,14 @@
 package loader
 
 import (
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
 	"strconv"
 
 	gorillaws "github.com/gorilla/websocket"
+	"github.com/porter-dev/api-contracts/generated/go/porter/v1/porterv1connect"
 	"github.com/porter-dev/porter/api/server/shared/apierrors/alerter"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"github.com/porter-dev/porter/api/server/shared/config/env"
@@ -226,6 +228,12 @@ func (e *EnvConfigLoader) LoadConfig() (res *config.Config, err error) {
 		res.PowerDNSClient = powerdns.NewClient(sc.PowerDNSAPIServerURL, sc.PowerDNSAPIKey, sc.AppRootDomain)
 	}
 
+	if sc.ClusterControlPlaneAddress == "" {
+		return res, errors.New("must provide CLUSTER_CONTROL_PLANE_ADDRESS")
+	}
+	client := porterv1connect.NewClusterControlPlaneServiceClient(http.DefaultClient, sc.ClusterControlPlaneAddress)
+	res.ClusterControlPlaneClient = client
+
 	return res, nil
 }
 

+ 1 - 1
docker/Dockerfile

@@ -2,7 +2,7 @@
 
 # Base Go environment
 # -------------------
-FROM golang:1.18-alpine as base
+FROM golang:1.20-alpine as base
 WORKDIR /porter
 
 RUN apk update && apk add --no-cache gcc musl-dev git protoc

+ 1 - 1
docker/cli.Dockerfile

@@ -2,7 +2,7 @@
 
 # Base Go environment
 # -------------------
-FROM golang:1.18 as base
+FROM golang:1.20 as base
 WORKDIR /porter
 
 RUN apt-get update && apt-get install -y gcc musl-dev git make

+ 1 - 1
docker/dev.Dockerfile

@@ -1,6 +1,6 @@
 # Development environment
 # -----------------------
-FROM golang:1.18-alpine
+FROM golang:1.20-alpine
 WORKDIR /porter
 
 RUN apk update && apk add --no-cache gcc musl-dev git

+ 3 - 1
go.mod

@@ -1,6 +1,6 @@
 module github.com/porter-dev/porter
 
-go 1.18
+go 1.20
 
 require (
 	cloud.google.com/go v0.105.0 // indirect
@@ -70,8 +70,10 @@ require (
 	github.com/Azure/azure-sdk-for-go/sdk/azcore v0.23.1
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v0.5.0
 	github.com/briandowns/spinner v1.18.1
+	github.com/bufbuild/connect-go v1.5.2
 	github.com/glebarez/sqlite v1.6.0
 	github.com/open-policy-agent/opa v0.44.0
+	github.com/porter-dev/api-contracts v0.0.10
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d
 	github.com/xanzy/go-gitlab v0.68.0

+ 8 - 0
go.sum

@@ -299,6 +299,8 @@ github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gL
 github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU=
 github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
 github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=
+github.com/bufbuild/connect-go v1.5.2 h1:G4EZd5gF1U1ZhhbVJXplbuUnfKpBZ5j5izqIwu2g2W8=
+github.com/bufbuild/connect-go v1.5.2/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk=
 github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
 github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
 github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng=
@@ -1454,6 +1456,12 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 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/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
+github.com/porter-dev/api-contracts v0.0.7 h1:4uGOb/jIg/oYnEEzqLgXrgHjFoVItAnmCpbEriUcbe0=
+github.com/porter-dev/api-contracts v0.0.7/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
+github.com/porter-dev/api-contracts v0.0.9 h1:MU7Ak8KPhi6LfmmfLQDJ0PAbQL/zuEpUibXZkUY7xM8=
+github.com/porter-dev/api-contracts v0.0.9/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
+github.com/porter-dev/api-contracts v0.0.10 h1:TIlyVtrufoJh9mnbOGlIuf1w99/8YWppBVzFWcRHI8c=
+github.com/porter-dev/api-contracts v0.0.10/go.mod h1:qr2L58mJLr5DUGV5OPw3REiSrQvJq6TgkKyEWP95dyU=
 github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935 h1:hfb3nt3AJXIBbevu6ARTg9SdOkMP6WLbKBiG5hT5rcc=
 github.com/porter-dev/switchboard v0.0.0-20221019155755-67ff2bf04935/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 1 - 5
go.work.sum

@@ -1,21 +1,17 @@
 cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU=
 cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
 github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
+github.com/bufbuild/connect-go v1.5.2 h1:G4EZd5gF1U1ZhhbVJXplbuUnfKpBZ5j5izqIwu2g2W8=
 github.com/containerd/stargz-snapshotter v0.11.3 h1:D3PoF563XmOBdtfx2G6AkhbHueqwIVPBFn2mrsWLa3w=
 github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
 github.com/go-redis/redis v6.15.8+incompatible h1:BKZuG6mCnRj5AOaWJXoCgf6rqTYnYJLe4en2hxT7r9o=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-github/v50 v50.0.0 h1:gdO1AeuSZZK4iYWwVbjni7zg8PIQhp7QfmPunr016Jk=
-github.com/google/go-github/v50 v50.0.0/go.mod h1:Ev4Tre8QoKiolvbpOSG3FIi4Mlon3S2Nt9W5JYqKiwA=
 github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
 github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
 github.com/porter-dev/porter v0.44.0/go.mod h1:GoIoc3h08jxGcgCwsTq+C6dt6jv6mO9OQRdZBrt8iR4=
 github.com/tchap/go-patricia v2.2.6+incompatible h1:JvoDL7JSoIP2HDE8AbDH3zC8QBPxmzYe32HHy5yQ+Ck=
-golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
 golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
 golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
 golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg=
 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

+ 7 - 0
internal/helm/agent.go

@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"runtime/debug"
 	"strconv"
 	"strings"
 	"time"
@@ -405,6 +406,12 @@ func (a *Agent) InstallChart(
 	doAuth *oauth2.Config,
 	disablePullSecretsInjection bool,
 ) (*release.Release, error) {
+	defer func() {
+		if r := recover(); r != nil {
+			fmt.Println("stacktrace from panic: \n" + string(debug.Stack()))
+		}
+	}()
+
 	cmd := action.NewInstall(a.ActionConfig)
 
 	if cmd.Version == "" && cmd.Devel {

+ 99 - 4
internal/kubernetes/config.go

@@ -1,13 +1,18 @@
 package kubernetes
 
 import (
+	"context"
 	"errors"
 	"fmt"
+	"os"
 	"path/filepath"
 	"regexp"
 	"strings"
 	"time"
 
+	"github.com/bufbuild/connect-go"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/api-contracts/generated/go/porter/v1/porterv1connect"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository"
@@ -62,19 +67,92 @@ func GetAgentOutOfClusterConfig(conf *OutOfClusterConfig) (*Agent, error) {
 		return GetAgentInClusterConfig(conf.DefaultNamespace)
 	}
 
-	restConf, err := conf.ToRESTConfig()
+	var restConf *rest.Config
+
+	if conf.Cluster.ProvisionedBy == "CAPI" {
+		rc, err := restConfigForCAPICluster(context.Background(), conf.CAPIManagementClusterClient, *conf.Cluster)
+		if err != nil {
+			return nil, err
+		}
+		restConf = rc
+	} else {
+		rc, err := conf.ToRESTConfig()
+		if err != nil {
+			return nil, err
+		}
+		restConf = rc
+	}
+
+	if restConf == nil {
+		return nil, fmt.Errorf("error getting rest config for cluster %s", conf.Cluster.ProvisionedBy)
+	}
 
+	clientset, err := kubernetes.NewForConfig(restConf)
 	if err != nil {
 		return nil, err
 	}
 
-	clientset, err := kubernetes.NewForConfig(restConf)
+	return &Agent{conf, clientset}, nil
+}
 
+// restConfigForCAPICluster gets the kubernetes rest API client for a CAPI cluster
+func restConfigForCAPICluster(ctx context.Context, mgmtClusterConnection porterv1connect.ClusterControlPlaneServiceClient, cluster models.Cluster) (*rest.Config, error) {
+	kc, err := kubeConfigForCAPICluster(ctx, mgmtClusterConnection, cluster)
 	if err != nil {
 		return nil, err
 	}
 
-	return &Agent{conf, clientset}, nil
+	rc, err := writeKubeConfigToFileAndRestClient([]byte(kc))
+	if err != nil {
+		return nil, err
+	}
+	return rc, nil
+}
+
+// kubeConfigForCAPICluster grabs the raw kube config for a capi cluster
+func kubeConfigForCAPICluster(ctx context.Context, mgmtClusterConnection porterv1connect.ClusterControlPlaneServiceClient, cluster models.Cluster) (string, error) {
+	kubeconfigResp, err := mgmtClusterConnection.KubeConfigForCluster(context.Background(), connect.NewRequest(
+		&porterv1.KubeConfigForClusterRequest{
+			ProjectId: int64(cluster.ProjectID),
+			ClusterId: int64(cluster.ID),
+		},
+	))
+	if err != nil {
+		return "", fmt.Errorf("error getting capi config: %w", err)
+	}
+	if kubeconfigResp.Msg == nil {
+		return "", errors.New("no kubeconfig returned for capi cluster")
+	}
+	if kubeconfigResp.Msg.KubeConfig == "" {
+		return "", errors.New("no kubeconfig returned for capi cluster")
+	}
+	return kubeconfigResp.Msg.KubeConfig, nil
+}
+
+// writeKubeConfigToFileAndRestClient writes a literal kubeconfig to a temporary file
+// then uses the client-go kubernetes package to create a rest.Config from it
+func writeKubeConfigToFileAndRestClient(kubeconf []byte) (*rest.Config, error) {
+	tmpFile, err := os.CreateTemp(os.TempDir(), "kconf-")
+	if err != nil {
+		return nil, fmt.Errorf("unable to create temp file: %w", err)
+	}
+	defer os.Remove(tmpFile.Name())
+
+	if _, err = tmpFile.Write(kubeconf); err != nil {
+		return nil, fmt.Errorf("unable to write to temp file: %w", err)
+	}
+	if err := tmpFile.Close(); err != nil {
+		return nil, fmt.Errorf("unable to close temp file: %w", err)
+	}
+	kconfPath, err := filepath.Abs(tmpFile.Name())
+	if err != nil {
+		return nil, fmt.Errorf("unable to find temp file: %w", err)
+	}
+	rest, err := clientcmd.BuildConfigFromFlags("", kconfPath)
+	if err != nil {
+		return nil, fmt.Errorf("unable create rest config from temp file: %w", err)
+	}
+	return rest, nil
 }
 
 // IsInCluster returns true if the process is running in a Kubernetes cluster,
@@ -118,14 +196,23 @@ type OutOfClusterConfig struct {
 
 	// Only required if using DigitalOcean OAuth as an auth mechanism
 	DigitalOceanOAuth *oauth2.Config
+
+	CAPIManagementClusterClient porterv1connect.ClusterControlPlaneServiceClient
 }
 
 // ToRESTConfig creates a kubernetes REST client factory -- it calls ClientConfig on
 // the result of ToRawKubeConfigLoader, and also adds a custom http transport layer
 // if necessary (required for GCP auth)
 func (conf *OutOfClusterConfig) ToRESTConfig() (*rest.Config, error) {
-	cmdConf, err := conf.GetClientConfigFromCluster()
+	if conf.Cluster.ProvisionedBy == "CAPI" {
+		rc, err := restConfigForCAPICluster(context.Background(), conf.CAPIManagementClusterClient, *conf.Cluster)
+		if err != nil {
+			return nil, err
+		}
+		return rc, nil
+	}
 
+	cmdConf, err := conf.GetClientConfigFromCluster()
 	if err != nil {
 		return nil, err
 	}
@@ -194,6 +281,14 @@ func (conf *OutOfClusterConfig) GetClientConfigFromCluster() (clientcmd.ClientCo
 		return nil, fmt.Errorf("cluster cannot be nil")
 	}
 
+	if conf.Cluster.ProvisionedBy == "CAPI" {
+		rc, err := kubeConfigForCAPICluster(context.Background(), conf.CAPIManagementClusterClient, *conf.Cluster)
+		if err != nil {
+			return nil, err
+		}
+		return clientcmd.NewClientConfigFromBytes([]byte(rc))
+	}
+
 	if conf.Cluster.AuthMechanism == models.Local {
 		kubeAuth, err := conf.Repo.KubeIntegration().ReadKubeIntegration(
 			conf.Cluster.ProjectID,

+ 32 - 0
internal/models/aws_assume_role_chain.go

@@ -0,0 +1,32 @@
+package models
+
+import (
+	"github.com/google/uuid"
+	"gorm.io/gorm"
+)
+
+// AWSAssumeRoleChain represents an assume role chain link
+type AWSAssumeRoleChain struct {
+	gorm.Model
+
+	// ID is a UUID for the CAPI Cluster's config
+	ID uuid.UUID `gorm:"type:uuid;primaryKey"`
+
+	// SourceARN is ARN which will assume the target ARN
+	SourceARN string `json:"source_arn"`
+
+	// TargetARN is ARN which will assume the target ARN
+	TargetARN string `json:"target_arn"`
+
+	// ExternalID is ID which is required when assuming a role
+	ExternalID string `json:"external_id"`
+
+	// ProjectID is the ID of the project that the config belongs to.
+	// This should be a foreign key, but GORM doesnt play well with FKs.
+	ProjectID int
+}
+
+// TableName overrides the table name
+func (AWSAssumeRoleChain) TableName() string {
+	return "aws_assume_role_chains"
+}

+ 30 - 0
internal/models/capi_config.go

@@ -0,0 +1,30 @@
+package models
+
+import (
+	"github.com/google/uuid"
+	"gorm.io/gorm"
+)
+
+// CAPIConfig represents a ClusterAPI base64 encoded config
+type CAPIConfig struct {
+	gorm.Model
+
+	// ID is a UUID for the CAPI Cluster's config
+	ID uuid.UUID `gorm:"type:uuid;primaryKey"`
+
+	// Base64Config is the CAPI config for a cluster, encoded in base64
+	Base64Config string
+
+	// ClusterID is the ID of the cluster that the config created.
+	// This should be a foreign key, but GORM doesnt play well with FKs.
+	ClusterID int
+
+	// ProjectID is the ID of the project that the config belongs to.
+	// This should be a foreign key, but GORM doesnt play well with FKs.
+	ProjectID int
+}
+
+// TableName overrides the table name
+func (CAPIConfig) TableName() string {
+	return "capi_configs"
+}

+ 17 - 0
internal/models/cluster.go

@@ -42,6 +42,9 @@ type Cluster struct {
 	// Name of the cluster
 	Name string `json:"name"`
 
+	// VanityName allows for a display-only name without changing how the cluster looks
+	VanityName string `json:"vanity_name"`
+
 	// Server endpoint for the cluster
 	Server string `json:"server"`
 
@@ -62,6 +65,20 @@ type Cluster struct {
 
 	AWSClusterID string
 
+	// Status defines the current status of the cluster. Accepted values: [READY, UPDATING]
+	Status string `json:"status"`
+
+	// ProvisionedBy is used for identifing the provisioner used for the cluster. Accepted values: [CAPI, ]
+	ProvisionedBy string `json:"provisioned_by"`
+
+	// CloudProvider is the cloud provider that hosts the Kubernetes Cluster. Accepted values: [AWS, GCP, AZURE]
+	CloudProvider string `json:"cloud_provider"`
+
+	// CloudProviderCredentialIdentifier is a reference to find the credentials required for access the cluster's API.
+	// This was likely the credential that was used to create the cluster.
+	// For AWS EKS clusters, this will be an ARN for the final target role in the assume role chain.
+	CloudProviderCredentialIdentifier string `json:"cloud_provider_credential_identifier"`
+
 	// ------------------------------------------------------------------
 	// All fields below this line are encrypted before storage
 	// ------------------------------------------------------------------

+ 2 - 0
internal/repository/gorm/migrate.go

@@ -58,6 +58,8 @@ func AutoMigrate(db *gorm.DB, debug bool) error {
 		&models.StackEnvGroup{},
 		&models.DbMigration{},
 		&models.MonitorTestResult{},
+		&models.CAPIConfig{},
+		&models.AWSAssumeRoleChain{},
 		&ints.KubeIntegration{},
 		&ints.BasicIntegration{},
 		&ints.OIDCIntegration{},