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

display provisioner if provisioning

jusrhee 5 лет назад
Родитель
Сommit
ade30cf7fc

+ 42 - 0
.gitignore

@@ -9,3 +9,45 @@ gon.hcl
 internal/local_templates
 gon*.hcl
 *prod.Dockerfile
+
+# Local .terraform directories
+**/.terraform/*
+
+.terraform
+
+.terraform.lock.hcl
+
+*kubeconfig*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+
+# Exclude all .tfvars files, which are likely to contain sentitive data, such as
+# password, private keys, and other secrets. These should not be part of version 
+# control as they are data points which are potentially sensitive and subject 
+# to change depending on the environment.
+#
+*.tfvars
+*.tfvars.json
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+#
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc

+ 5 - 6
cmd/app/main.go

@@ -76,10 +76,10 @@ func main() {
 	repo := gorm.NewRepository(db, &key)
 
 	a, _ := api.New(&api.AppConfig{
-		Logger:      logger,
-		Repository:  repo,
-		ServerConf:  appConf.Server,
-		RedisClient: redis,
+		Logger:     logger,
+		Repository: repo,
+		ServerConf: appConf.Server,
+		RedisConf:  &appConf.Redis,
 	})
 
 	appRouter := router.New(a)
@@ -98,10 +98,9 @@ func main() {
 
 	errorChan := make(chan error)
 
-	go prov.GlobalStreamListener(redis, repo.AWSInfra, errorChan)
+	go prov.GlobalStreamListener(redis, *repo, errorChan)
 
 	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
 		log.Fatal("Server startup failed", err)
 	}
-
 }

+ 24 - 4
dashboard/src/main/home/Home.tsx

@@ -4,7 +4,7 @@ import ReactModal from 'react-modal';
 
 import { Context } from '../../shared/Context';
 import api from '../../shared/api';
-import { ProjectType } from '../../shared/types';
+import { InfraType } from '../../shared/types';
 
 import Sidebar from './sidebar/Sidebar';
 import Dashboard from './dashboard/Dashboard';
@@ -28,6 +28,7 @@ type StateType = {
   forceSidebar: boolean,
   showWelcome: boolean,
   currentView: string,
+  viewData: any,
 
   // Track last project id for refreshing clusters on project change
   prevProjectId: number | null,
@@ -39,6 +40,7 @@ export default class Home extends Component<PropsType, StateType> {
     showWelcome: false,
     currentView: 'dashboard',
     prevProjectId: null as number | null,
+    viewData: null as any
   }
 
   // Possibly consolidate into context (w/ ProjectSection + NewProject)
@@ -46,11 +48,29 @@ export default class Home extends Component<PropsType, StateType> {
     let { user, currentProject, projects, setProjects } = this.context;
     api.getProjects('<token>', {}, { id: user.userId }, (err: any, res: any) => {
       if (err) {
-        console.log(err)
+        console.log(err);
       } else if (res.data) {
         setProjects(res.data);
         if (res.data.length > 0 && !currentProject) {
           this.context.setCurrentProject(res.data[0]);
+
+          // Check if current project is provisioning
+          api.getInfra('<token>', {}, { project_id: res.data[0].id }, (err: any, res: any) => {
+            if (err) {
+              console.log(err);
+            } else if (res.data) {
+              
+              // TODO: separately handle non meta-provisioning case
+              res.data.forEach((el: InfraType) => {
+                if (el.status === 'creating') {
+                  this.setState({ currentView: 'provisioner', viewData: {
+                    infra_id: el.id,
+                    kind: el.kind,
+                  }});
+                }
+              });
+            }
+          });
         } else if (res.data.length === 0) {
           this.setState({ currentView: 'new-project' });
         }
@@ -122,10 +142,10 @@ export default class Home extends Component<PropsType, StateType> {
       return <Integrations />;
     } else if (currentView === 'new-project') {
       return (
-        <NewProject setCurrentView={(x: string) => this.setState({ currentView: x })} />
+        <NewProject setCurrentView={(x: string, data: any ) => this.setState({ currentView: x, viewData: data })} />
       );
     } else if (currentView === 'provisioner') {
-      return <Provisioner />
+      return <Provisioner viewData={this.state.viewData}/>
     }
 
     return (

+ 1 - 1
dashboard/src/main/home/cluster-dashboard/expanded-chart/status/Logs.tsx

@@ -43,7 +43,7 @@ export default class Logs extends Component<PropsType, StateType> {
     if (!selectedPod.metadata?.name) return
     let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
     let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/k8s/${selectedPod?.metadata?.namespace}/pod/${selectedPod?.metadata?.name}/logs?cluster_id=${currentCluster.id}&service_account_id=${currentCluster.service_account_id}`)
-    // let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provisioning/ecr/abcdef/logs?cluster_id=${currentCluster.id}`)
+    
     this.setState({ ws }, () => {
       if (!this.state.ws) return;
   

+ 1 - 0
dashboard/src/main/home/integrations/integration-form/ECRForm.tsx

@@ -40,6 +40,7 @@ export default class ECRForm extends Component<PropsType, StateType> {
   handleSubmit = () => {
     let { awsRegion, awsAccessId, awsSecretKey, credentialsName } = this.state;
     let { currentProject } = this.context;
+
     api.createAWSIntegration('<token>', {
       aws_region: awsRegion,
       aws_access_key_id: awsAccessId,

+ 28 - 3
dashboard/src/main/home/new-project/NewProject.tsx

@@ -16,7 +16,7 @@ import SaveButton from '../../../components/SaveButton';
 const providers = ['aws', 'gcp', 'do',];
 
 type PropsType = {
-  setCurrentView: (x: string) => void,
+  setCurrentView: (x: string, data: any) => void,
 };
 
 type StateType = {
@@ -195,6 +195,8 @@ export default class NewProject extends Component<PropsType, StateType> {
 
   createProject = () => {
     this.setState({ status: 'loading' });
+    let { awsAccessId, awsSecretKey, awsRegion } = this.state;
+
     api.createProject('<token>', {
       name: this.state.projectName
     }, {}, (err: any, res: any) => {
@@ -213,9 +215,32 @@ export default class NewProject extends Component<PropsType, StateType> {
 
               // Handle provisioning logic
               if (this.state.selectedProvider === 'aws') {
-                this.props.setCurrentView('provisioner');
+                let clusterName = `${proj.name}-cluster`
+
+                api.createAWSIntegration('<token>', {
+                  aws_region: awsRegion,
+                  aws_cluster_id: clusterName,
+                  aws_access_key_id: awsAccessId,
+                  aws_secret_access_key: awsSecretKey,
+                }, { id: proj.id }, (err2: any, res2: any) => {
+                  if (err2) {
+                    console.log(err2);
+                  } else {
+                    api.provisionEKS('<token>', {
+                      aws_integration_id: res2.data.id,
+                      eks_name: clusterName,
+                    }, {id: proj.id}, (err3: any, res3:any) => {
+                      if (err3) {
+                        console.log(err3)
+                      } else {
+                        this.props.setCurrentView('provisioner', { infra_id: res3.data.id, kind: res3.data.kind });
+                      }
+                    })
+                  }
+                });
+
               } else {
-                this.props.setCurrentView('dashboard');
+                this.props.setCurrentView('dashboard', null);
               }
             } 
           }

+ 39 - 1
dashboard/src/main/home/new-project/Provisioner.tsx

@@ -9,10 +9,12 @@ import loading from '../../../assets/loading.gif';
 import Helper from '../../../components/values-form/Helper';
 
 type PropsType = {
+  viewData: any,
 };
 
 type StateType = {
   logs: string[],
+  ws: any
 };
 
 const loadMax = 40;
@@ -20,10 +22,46 @@ const loadMax = 40;
 export default class Provisioner extends Component<PropsType, StateType> {
   state = {
     logs: [] as string[],
+    ws : null as any
+  }
+
+  scrollToBottom = () => {
+    this.scrollRef.current.scrollTop = this.scrollRef.current.scrollHeight
   }
 
   componentDidMount() {
-    this.setState({ logs: ['test-1', 'test-2'] });
+    let { currentProject } = this.context;
+    let protocol = process.env.NODE_ENV == 'production' ? 'wss' : 'ws'
+    let ws = new WebSocket(`${protocol}://${process.env.API_SERVER}/api/projects/${currentProject.id}/provision/${this.props.viewData.kind}/${this.props.viewData.infra_id}/logs`)
+
+    this.setState({ ws }, () => {
+      if (!this.state.ws) return;
+  
+      this.state.ws.onopen = () => {
+        console.log('connected to websocket')
+      }
+  
+      this.state.ws.onmessage = (evt: MessageEvent) => {
+        let event = JSON.parse(evt.data)
+        let data = event.map((msg: any) => { return msg["Values"]["data"]})
+        this.setState({ logs: [...this.state.logs, ...data] }, () => {
+          this.scrollToBottom()
+        })
+      }
+  
+      this.state.ws.onerror = (err: ErrorEvent) => {
+        console.log(err)
+      }
+    })
+
+    this.setState({ logs: [] });
+  }
+
+  componentWillUnmount() {
+    if (this.state.ws) {
+      console.log('closing websocket')
+      this.state.ws.close()
+    }
   }
 
   scrollRef = React.createRef<HTMLDivElement>();

+ 25 - 0
dashboard/src/shared/api.tsx

@@ -210,12 +210,27 @@ const getProjectRepos = baseApi<{}, { id: number }>('GET', pathParams => {
 
 const createAWSIntegration = baseApi<{
   aws_region: string,
+  aws_cluster_id?: string,
   aws_access_key_id: string,
   aws_secret_access_key: string,
 }, { id: number }>('POST', pathParams => {
   return `/api/projects/${pathParams.id}/integrations/aws`;
 });
 
+const provisionECR = baseApi<{
+  ecr_name: string,
+  aws_integration_id: string,
+}, { id: number }>('POST', pathParams => {
+  return `/api/projects/${pathParams.id}/provision/ecr`;
+});
+
+const provisionEKS = baseApi<{
+  eks_name: string,
+  aws_integration_id: string,
+}, { id: number }>('POST', pathParams => {
+  return `/api/projects/${pathParams.id}/provision/eks`;
+});
+
 const createECR = baseApi<{
   name: string,
   aws_integration_id: string,
@@ -254,8 +269,16 @@ const getGitRepos = baseApi<{
   return `/api/projects/${pathParams.project_id}/gitrepos`;
 });
 
+const getInfra = baseApi<{
+}, {
+  project_id: number,
+}>('GET', pathParams => {
+  return `/api/projects/${pathParams.project_id}/infra`;
+});
+
 // Bundle export to allow default api import (api.<method> is more readable)
 export default {
+  getInfra,
   linkGithubProject,
   getGitRepos,
   checkAuth,
@@ -292,6 +315,8 @@ export default {
   getProjectRegistries,
   getProjectRepos,
   createAWSIntegration,
+  provisionECR,
+  provisionEKS,
   createECR,
   getImageRepos,
   getImageTags,

+ 7 - 0
dashboard/src/shared/types.tsx

@@ -140,4 +140,11 @@ export interface ImageType {
   source: string,
   registryId: number,
   name: string,
+}
+
+export interface InfraType {
+  id: number,
+  project_d: number,
+  kind: string,
+  status: string,
 }

+ 4 - 4
internal/adapter/redis.go

@@ -11,10 +11,10 @@ import (
 // NewRedisClient returns a new redis client instance
 func NewRedisClient(conf *config.RedisConf) (*redis.Client, error) {
 	client := redis.NewClient(&redis.Options{
-		Addr: fmt.Sprintf("%s:%s", conf.Host, conf.Port),
-		// Username: conf.Username,
-		// Password: conf.Password,
-		// DB:       conf.DB,
+		Addr:     fmt.Sprintf("%s:%s", conf.Host, conf.Port),
+		Username: conf.Username,
+		Password: conf.Password,
+		DB:       conf.DB,
 	})
 
 	_, err := client.Ping(context.Background()).Result()

+ 5 - 2
internal/config/redis.go

@@ -2,6 +2,9 @@ package config
 
 // RedisConf is the redis config required for the provisioner container
 type RedisConf struct {
-	Host string `env:"REDIS_HOST,default=redis"`
-	Port string `env:"REDIS_PORT,default=6379"`
+	Host     string `env:"REDIS_HOST,default=redis"`
+	Port     string `env:"REDIS_PORT,default=6379"`
+	Username string `env:"REDIS_USER"`
+	Password string `env:"REDIS_PASS"`
+	DB       int    `env:"REDIS_DB,default=0"`
 }

+ 18 - 0
internal/forms/infra.go

@@ -21,3 +21,21 @@ func (ce *CreateECRInfra) ToAWSInfra() (*models.AWSInfra, error) {
 		AWSIntegrationID: ce.AWSIntegrationID,
 	}, nil
 }
+
+// CreateEKSInfra represents the accepted values for creating an
+// EKS infra via the provisioning container
+type CreateEKSInfra struct {
+	EKSName          string `json:"eks_name" form:"required"`
+	ProjectID        uint   `json:"project_id" form:"required"`
+	AWSIntegrationID uint   `json:"aws_integration_id" form:"required"`
+}
+
+// ToAWSInfra converts the form to a gorm aws infra model
+func (ce *CreateEKSInfra) ToAWSInfra() (*models.AWSInfra, error) {
+	return &models.AWSInfra{
+		Kind:             models.AWSInfraEKS,
+		ProjectID:        ce.ProjectID,
+		Status:           models.StatusCreating,
+		AWSIntegrationID: ce.AWSIntegrationID,
+	}, nil
+}

+ 31 - 2
internal/kubernetes/agent.go

@@ -11,6 +11,8 @@ import (
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/ecr"
+	"github.com/porter-dev/porter/internal/kubernetes/provisioner/aws/eks"
+	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models/integrations"
 
 	"github.com/gorilla/websocket"
@@ -236,10 +238,12 @@ func (a *Agent) ProvisionECR(
 	projectID uint,
 	awsConf *integrations.AWSIntegration,
 	ecrName string,
+	awsInfra *models.AWSInfra,
 ) (*batchv1.Job, error) {
+	id := awsInfra.GetID()
 	prov := &provisioner.Conf{
-		ID:   fmt.Sprintf("%s-%d", ecrName, projectID),
-		Name: fmt.Sprintf("prov-%s-%d", ecrName, projectID),
+		ID:   id,
+		Name: fmt.Sprintf("prov-%s", id),
 		Kind: provisioner.ECR,
 		AWS: &aws.Conf{
 			AWSRegion:          awsConf.AWSRegion,
@@ -254,6 +258,31 @@ func (a *Agent) ProvisionECR(
 	return a.provision(prov)
 }
 
+// ProvisionEKS spawns a new provisioning pod that creates an EKS instance
+func (a *Agent) ProvisionEKS(
+	projectID uint,
+	awsConf *integrations.AWSIntegration,
+	eksName string,
+	awsInfra *models.AWSInfra,
+) (*batchv1.Job, error) {
+	id := awsInfra.GetID()
+	prov := &provisioner.Conf{
+		ID:   id,
+		Name: fmt.Sprintf("prov-%s", id),
+		Kind: provisioner.EKS,
+		AWS: &aws.Conf{
+			AWSRegion:          awsConf.AWSRegion,
+			AWSAccessKeyID:     string(awsConf.AWSAccessKeyID),
+			AWSSecretAccessKey: string(awsConf.AWSSecretAccessKey),
+		},
+		EKS: &eks.Conf{
+			ClusterName: eksName,
+		},
+	}
+
+	return a.provision(prov)
+}
+
 // ProvisionTest spawns a new provisioning pod that tests provisioning
 func (a *Agent) ProvisionTest(
 	projectID uint,

+ 87 - 7
internal/kubernetes/provisioner/global_stream.go

@@ -2,7 +2,10 @@ package provisioner
 
 import (
 	"context"
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
+	"regexp"
 
 	"github.com/porter-dev/porter/internal/repository"
 
@@ -45,6 +48,8 @@ func InitGlobalStream(client *redis.Client) error {
 		GlobalStreamName,
 	).Result()
 
+	fmt.Println(xInfoGroups, err)
+
 	if err != nil {
 		return err
 	}
@@ -52,6 +57,7 @@ func InitGlobalStream(client *redis.Client) error {
 	for _, group := range xInfoGroups {
 		// if the group exists, return with no error
 		if group.Name == GlobalStreamGroupName {
+			fmt.Println("group already exists")
 			return nil
 		}
 	}
@@ -61,9 +67,11 @@ func InitGlobalStream(client *redis.Client) error {
 		context.Background(),
 		GlobalStreamName,
 		GlobalStreamGroupName,
-		">",
+		"$",
 	).Result()
 
+	fmt.Println("xgroup created", err)
+
 	return err
 }
 
@@ -72,13 +80,15 @@ type ResourceCRUDHandler interface {
 	OnCreate(id uint) error
 }
 
-// GlobalStreamListener performs an XREADGROUP operation on a given stream
-// and sends a GlobalStreamMessage to the msgChan
+// GlobalStreamListener performs an XREADGROUP operation on a given stream and
+// updates models in the database as necessary
 func GlobalStreamListener(
 	client *redis.Client,
-	infraRepo repository.AWSInfraRepository,
+	repo repository.Repository,
 	errorChan chan error,
 ) {
+	fmt.Println("starting global stream listener")
+
 	for {
 		xstreams, err := client.XReadGroup(
 			context.Background(),
@@ -90,6 +100,8 @@ func GlobalStreamListener(
 			},
 		).Result()
 
+		fmt.Println(xstreams, err)
+
 		if err != nil {
 			errorChan <- err
 			return
@@ -98,10 +110,10 @@ func GlobalStreamListener(
 		// parse messages from the global stream
 		for _, msg := range xstreams[0].Messages {
 			// parse the id to identify the infra
-			infraID, err := models.GetInfraIDFromWorkspaceID(fmt.Sprintf("%v", msg.Values["id"]))
+			kind, projID, infraID, err := models.ParseWorkspaceID(fmt.Sprintf("%v", msg.Values["id"]))
 
 			if fmt.Sprintf("%v", msg.Values["status"]) == "created" {
-				infra, err := infraRepo.ReadAWSInfra(infraID)
+				infra, err := repo.AWSInfra.ReadAWSInfra(infraID)
 
 				if err != nil {
 					continue
@@ -109,7 +121,75 @@ func GlobalStreamListener(
 
 				infra.Status = models.StatusCreated
 
-				infra, err = infraRepo.UpdateAWSInfra(infra)
+				infra, err = repo.AWSInfra.UpdateAWSInfra(infra)
+
+				if err != nil {
+					continue
+				}
+
+				// create ECR/EKS
+				if kind == string(models.AWSInfraECR) {
+					reg := &models.Registry{
+						ProjectID:        projID,
+						AWSIntegrationID: infra.AWSIntegrationID,
+					}
+
+					// parse raw data into ECR type
+					dataString, ok := msg.Values["data"].(string)
+
+					if ok {
+						json.Unmarshal([]byte(dataString), reg)
+					}
+
+					reg, err := repo.Registry.CreateRegistry(reg)
+
+					if err != nil {
+						continue
+					}
+				} else if kind == string(models.AWSInfraEKS) {
+					cluster := &models.Cluster{
+						AuthMechanism:    models.AWS,
+						ProjectID:        projID,
+						AWSIntegrationID: infra.AWSIntegrationID,
+					}
+
+					// parse raw data into ECR type
+					dataString, ok := msg.Values["data"].(string)
+
+					if ok {
+						json.Unmarshal([]byte(dataString), cluster)
+					}
+
+					re := regexp.MustCompile(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$`)
+
+					// if it matches the base64 regex, decode it
+					caData := string(cluster.CertificateAuthorityData)
+					if re.MatchString(caData) {
+						decoded, err := base64.StdEncoding.DecodeString(caData)
+
+						if err != nil {
+							continue
+						}
+
+						cluster.CertificateAuthorityData = []byte(decoded)
+					}
+
+					cluster, err := repo.Cluster.CreateCluster(cluster)
+
+					if err != nil {
+						continue
+					}
+				}
+			} else if fmt.Sprintf("%v", msg.Values["status"]) == "error" {
+				infra, err := repo.AWSInfra.ReadAWSInfra(infraID)
+
+				if err != nil {
+					continue
+				}
+
+				infra.Status = models.StatusError
+
+				infra, err = repo.AWSInfra.UpdateAWSInfra(infra)
 
 				if err != nil {
 					continue

+ 2 - 1
internal/kubernetes/provisioner/resource_stream.go

@@ -39,13 +39,14 @@ func ResourceStream(client *redis.Client, streamName string, conn *websocket.Con
 			).Result()
 
 			if err != nil {
+				fmt.Println("ERROR XREAD", err)
 				return
 			}
 
 			messages := xstream[0].Messages
 			lastID = messages[len(messages)-1].ID
 
-			if writeErr := conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprint(xstream))); writeErr != nil {
+			if writeErr := conn.WriteJSON(messages); writeErr != nil {
 				errorchan <- writeErr
 				return
 			}

+ 15 - 8
internal/models/infra.go

@@ -15,6 +15,7 @@ type InfraStatus string
 const (
 	StatusCreating InfraStatus = "creating"
 	StatusCreated  InfraStatus = "created"
+	StatusError    InfraStatus = "error"
 )
 
 // AWSInfraKind is the kind that aws infra can be
@@ -68,24 +69,30 @@ func (ai *AWSInfra) Externalize() *AWSInfraExternal {
 	}
 }
 
-// GetWorkspaceID returns the unique workspace id for this infra
-func (ai *AWSInfra) GetWorkspaceID() string {
+// GetID returns the unique id for this infra
+func (ai *AWSInfra) GetID() string {
 	return fmt.Sprintf("%s-%d-%d", ai.Kind, ai.ProjectID, ai.ID)
 }
 
-// GetInfraIDFromWorkspaceID returns the infra id given a workspace id
-func GetInfraIDFromWorkspaceID(workspaceID string) (uint, error) {
+// ParseWorkspaceID returns the (kind, projectID, infraID)
+func ParseWorkspaceID(workspaceID string) (string, uint, uint, error) {
 	strArr := strings.Split(workspaceID, "-")
 
 	if len(strArr) != 3 {
-		return 0, fmt.Errorf("workspace id improperly formatted")
+		return "", 0, 0, fmt.Errorf("workspace id improperly formatted")
 	}
 
-	u, err := strconv.ParseUint(strArr[2], 10, 64)
+	projID, err := strconv.ParseUint(strArr[1], 10, 64)
 
 	if err != nil {
-		return 0, err
+		return "", 0, 0, err
 	}
 
-	return uint(u), nil
+	infraID, err := strconv.ParseUint(strArr[2], 10, 64)
+
+	if err != nil {
+		return "", 0, 0, err
+	}
+
+	return strArr[0], uint(projID), uint(infraID), nil
 }

+ 15 - 16
server/api/api.go

@@ -6,7 +6,6 @@ import (
 	"github.com/go-playground/locales/en"
 	ut "github.com/go-playground/universal-translator"
 	vr "github.com/go-playground/validator/v10"
-	"github.com/go-redis/redis/v8"
 	sessionstore "github.com/porter-dev/porter/internal/auth"
 	"github.com/porter-dev/porter/internal/oauth"
 	"golang.org/x/oauth2"
@@ -33,11 +32,11 @@ type TestAgents struct {
 
 // AppConfig is the configuration required for creating a new App
 type AppConfig struct {
-	DB          *gorm.DB
-	Logger      *lr.Logger
-	Repository  *repository.Repository
-	ServerConf  config.ServerConf
-	RedisClient *redis.Client
+	DB         *gorm.DB
+	Logger     *lr.Logger
+	Repository *repository.Repository
+	ServerConf config.ServerConf
+	RedisConf  *config.RedisConf
 
 	// TestAgents if API is in testing mode
 	TestAgents *TestAgents
@@ -61,8 +60,8 @@ type App struct {
 	// agents exposed for testing
 	TestAgents *TestAgents
 
-	// redis conf for redis connection
-	RedisClient *redis.Client
+	// redis client for redis connection
+	RedisConf *config.RedisConf
 
 	// oauth-specific clients
 	GithubConf *oauth2.Config
@@ -86,14 +85,14 @@ func New(conf *AppConfig) (*App, error) {
 	}
 
 	app := &App{
-		Logger:      conf.Logger,
-		Repo:        conf.Repository,
-		ServerConf:  conf.ServerConf,
-		RedisClient: conf.RedisClient,
-		TestAgents:  conf.TestAgents,
-		db:          conf.DB,
-		validator:   validator,
-		translator:  &translator,
+		Logger:     conf.Logger,
+		Repo:       conf.Repository,
+		ServerConf: conf.ServerConf,
+		RedisConf:  conf.RedisConf,
+		TestAgents: conf.TestAgents,
+		db:         conf.DB,
+		validator:  validator,
+		translator: &translator,
 	}
 
 	// if repository not specified, default to in-memory

+ 40 - 0
server/api/infra_handler.go

@@ -0,0 +1,40 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+	"strconv"
+
+	"github.com/go-chi/chi"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+// HandleListProjectInfra returns a list of infrasa for a project
+func (app *App) HandleListProjectInfra(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	infras, err := app.Repo.AWSInfra.ListAWSInfrasByProjectID(uint(projID))
+
+	if err != nil {
+		app.handleErrorRead(err, ErrProjectDataRead, w)
+		return
+	}
+
+	extInfras := make([]*models.AWSInfraExternal, 0)
+
+	for _, infra := range infras {
+		extInfras = append(extInfras, infra.Externalize())
+	}
+
+	w.WriteHeader(http.StatusOK)
+
+	if err := json.NewEncoder(w).Encode(extInfras); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}

+ 91 - 1
server/api/provision_handler.go

@@ -11,6 +11,8 @@ import (
 	"github.com/porter-dev/porter/internal/forms"
 	"github.com/porter-dev/porter/internal/kubernetes"
 	"github.com/porter-dev/porter/internal/kubernetes/provisioner"
+
+	"github.com/porter-dev/porter/internal/adapter"
 )
 
 // HandleProvisionTest will create a test resource by deploying a provisioner
@@ -101,6 +103,7 @@ func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Reques
 		uint(projID),
 		awsInt,
 		form.ECRName,
+		infra,
 	)
 
 	if err != nil {
@@ -120,6 +123,86 @@ func (app *App) HandleProvisionAWSECRInfra(w http.ResponseWriter, r *http.Reques
 	}
 }
 
+// HandleProvisionAWSEKSInfra provisions a new aws EKS instance for a project
+func (app *App) HandleProvisionAWSEKSInfra(w http.ResponseWriter, r *http.Request) {
+	projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+	if err != nil || projID == 0 {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	form := &forms.CreateEKSInfra{
+		ProjectID: uint(projID),
+	}
+
+	// decode from JSON to form value
+	if err := json.NewDecoder(r.Body).Decode(form); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// validate the form
+	if err := app.validator.Struct(form); err != nil {
+		app.handleErrorFormValidation(err, ErrProjectValidateFields, w)
+		return
+	}
+
+	// convert the form to an aws infra instance
+	infra, err := form.ToAWSInfra()
+
+	if err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+
+	// handle write to the database
+	infra, err = app.Repo.AWSInfra.CreateAWSInfra(infra)
+
+	if err != nil {
+		app.handleErrorDataWrite(err, w)
+		return
+	}
+
+	awsInt, err := app.Repo.AWSIntegration.ReadAWSIntegration(infra.AWSIntegrationID)
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// launch provisioning pod
+	agent, err := kubernetes.GetAgentInClusterConfig()
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	_, err = agent.ProvisionEKS(
+		uint(projID),
+		awsInt,
+		form.EKSName,
+		infra,
+	)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	app.Logger.Info().Msgf("New aws eks infra created: %d", infra.ID)
+
+	w.WriteHeader(http.StatusCreated)
+
+	infraExt := infra.Externalize()
+
+	if err := json.NewEncoder(w).Encode(infraExt); err != nil {
+		app.handleErrorFormDecoding(err, ErrProjectDecode, w)
+		return
+	}
+}
+
 // HandleGetProvisioningLogs returns real-time logs of the provisioning process via websockets
 func (app *App) HandleGetProvisioningLogs(w http.ResponseWriter, r *http.Request) {
 	// get path parameters
@@ -138,7 +221,14 @@ func (app *App) HandleGetProvisioningLogs(w http.ResponseWriter, r *http.Request
 		app.handleErrorUpgradeWebsocket(err, w)
 	}
 
-	err = provisioner.ResourceStream(app.RedisClient, streamName, conn)
+	client, err := adapter.NewRedisClient(app.RedisConf)
+
+	if err != nil {
+		app.handleErrorInternal(err, w)
+		return
+	}
+
+	err = provisioner.ResourceStream(client, streamName, conn)
 
 	if err != nil {
 		app.handleErrorWebsocketWrite(err, w)

+ 23 - 4
server/router/router.go

@@ -177,11 +177,20 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
-		// /api/projects/{project_id}/provision routes
-
-		// TODO -- restrict this endpoint
+		// /api/projects/{project_id}/infra routes
 		r.Method(
 			"GET",
+			"/projects/{project_id}/infra",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleListProjectInfra, l),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
+		// /api/projects/{project_id}/provision routes
+		r.Method(
+			"POST",
 			"/projects/{project_id}/provision/test",
 			auth.DoesUserHaveProjectAccess(
 				requestlog.NewHandler(a.HandleProvisionTest, l),
@@ -191,7 +200,7 @@ func New(a *api.App) *chi.Mux {
 		)
 
 		r.Method(
-			"GET",
+			"POST",
 			"/projects/{project_id}/provision/ecr",
 			auth.DoesUserHaveProjectAccess(
 				requestlog.NewHandler(a.HandleProvisionAWSECRInfra, l),
@@ -200,6 +209,16 @@ func New(a *api.App) *chi.Mux {
 			),
 		)
 
+		r.Method(
+			"POST",
+			"/projects/{project_id}/provision/eks",
+			auth.DoesUserHaveProjectAccess(
+				requestlog.NewHandler(a.HandleProvisionAWSEKSInfra, l),
+				mw.URLParam,
+				mw.ReadAccess,
+			),
+		)
+
 		r.Method(
 			"GET",
 			"/projects/{project_id}/provision/{kind}/{infra_id}/logs",