2
0
Эх сурвалжийг харах

add deploy addon + app handlers

Alexander Belanger 4 жил өмнө
parent
commit
a57394d59b

+ 265 - 0
api/server/handlers/release/create.go

@@ -0,0 +1,265 @@
+package release
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/auth/token"
+	"github.com/porter-dev/porter/internal/helm"
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/integrations/ci/actions"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/registry"
+	"github.com/porter-dev/porter/internal/repository"
+	"helm.sh/helm/v3/pkg/release"
+)
+
+type CreateReleaseHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewCreateReleaseHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateReleaseHandler {
+	return &CreateReleaseHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *CreateReleaseHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	user, _ := r.Context().Value(types.UserScope).(*models.User)
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	namespace := r.Context().Value(types.NamespaceScope).(string)
+
+	helmAgent, err := c.GetHelmAgent(r, cluster)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	request := &types.CreateReleaseRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	chart, err := loader.LoadChartPublic(request.RepoURL, request.TemplateName, request.TemplateVersion)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	conf := &helm.InstallChartConfig{
+		Chart:      chart,
+		Name:       request.Name,
+		Namespace:  namespace,
+		Values:     request.Values,
+		Cluster:    cluster,
+		Repo:       c.Repo(),
+		Registries: registries,
+	}
+
+	helmRelease, err := helmAgent.InstallChart(conf, c.Config().DOConf)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error installing a new chart: %s", err.Error()),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+
+	release, err := createReleaseFromHelmRelease(c.Config(), cluster.ProjectID, cluster.ID, helmRelease)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	if request.GithubActionConfig != nil {
+		_, _, err := createGitAction(
+			c.Config(),
+			user.ID,
+			cluster.ProjectID,
+			cluster.ID,
+			request.GithubActionConfig,
+			request.Name,
+			namespace,
+			release,
+		)
+
+		if err != nil {
+			c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+			return
+		}
+	}
+}
+
+func createReleaseFromHelmRelease(
+	config *config.Config,
+	projectID, clusterID uint,
+	helmRelease *release.Release,
+) (*models.Release, error) {
+	token, err := repository.GenerateRandomBytes(16)
+
+	if err != nil {
+		return nil, err
+	}
+
+	// create release with webhook token in db
+	image, ok := helmRelease.Config["image"].(map[string]interface{})
+
+	if !ok {
+		return nil, fmt.Errorf("Could not find field image in config")
+	}
+
+	repository := image["repository"]
+	repoStr, ok := repository.(string)
+
+	if !ok {
+		return nil, fmt.Errorf("Could not find field repository in config")
+	}
+
+	release := &models.Release{
+		ClusterID:    clusterID,
+		ProjectID:    projectID,
+		Namespace:    helmRelease.Namespace,
+		Name:         helmRelease.Name,
+		WebhookToken: token,
+		ImageRepoURI: repoStr,
+	}
+
+	return config.Repo.Release().CreateRelease(release)
+}
+
+func createGitAction(
+	config *config.Config,
+	userID, projectID, clusterID uint,
+	request *types.CreateGitActionConfigRequest,
+	name, namespace string,
+	release *models.Release,
+) (*types.GitActionConfig, []byte, error) {
+	// if the registry was provisioned through Porter, create a repository if necessary
+	if request.RegistryID != 0 {
+		// read the registry
+		reg, err := config.Repo.Registry().ReadRegistry(projectID, request.RegistryID)
+
+		if err != nil {
+			return nil, nil, err
+		}
+
+		_reg := registry.Registry(*reg)
+		regAPI := &_reg
+
+		// parse the name from the registry
+		nameSpl := strings.Split(request.ImageRepoURI, "/")
+		repoName := nameSpl[len(nameSpl)-1]
+
+		err = regAPI.CreateRepository(config.Repo, repoName)
+
+		if err != nil {
+			return nil, nil, err
+		}
+	}
+
+	repoSplit := strings.Split(request.GitRepo, "/")
+
+	if len(repoSplit) != 2 {
+		return nil, nil, fmt.Errorf("invalid formatting of repo name")
+	}
+
+	// generate porter jwt token
+	jwt, err := token.GetTokenForAPI(userID, projectID)
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	encoded, err := jwt.EncodeToken(config.TokenConf)
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// create the commit in the git repo
+	gaRunner := &actions.GithubActions{
+		ServerURL:              config.ServerConf.ServerURL,
+		GithubOAuthIntegration: nil,
+		GithubAppID:            config.GithubAppConf.AppID,
+		GithubAppSecretPath:    config.GithubAppConf.SecretPath,
+		GithubInstallationID:   request.GitRepoID,
+		GitRepoName:            repoSplit[1],
+		GitRepoOwner:           repoSplit[0],
+		Repo:                   config.Repo,
+		ProjectID:              projectID,
+		ClusterID:              clusterID,
+		ReleaseName:            name,
+		GitBranch:              request.GitBranch,
+		DockerFilePath:         request.DockerfilePath,
+		FolderPath:             request.FolderPath,
+		ImageRepoURL:           request.ImageRepoURI,
+		PorterToken:            encoded,
+		Version:                "v0.1.0",
+		ShouldCreateWorkflow:   request.ShouldCreateWorkflow,
+	}
+
+	workflowYAML, err := gaRunner.Setup()
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if !request.ShouldCreateWorkflow {
+		return nil, workflowYAML, nil
+	}
+
+	// handle write to the database
+	ga, err := config.Repo.GitActionConfig().CreateGitActionConfig(&models.GitActionConfig{
+		ReleaseID:    release.ID,
+		GitRepo:      request.GitRepo,
+		GitBranch:    request.GitBranch,
+		ImageRepoURI: request.ImageRepoURI,
+		// TODO: github installation id here?
+		GitRepoID:      request.GitRepoID,
+		DockerfilePath: request.DockerfilePath,
+		FolderPath:     request.FolderPath,
+		IsInstallation: true,
+		Version:        "v0.1.0",
+	})
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// update the release in the db with the image repo uri
+	release.ImageRepoURI = ga.ImageRepoURI
+
+	_, err = config.Repo.Release().UpdateRelease(release)
+
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return ga.ToGitActionConfigType(), workflowYAML, nil
+}

+ 85 - 0
api/server/handlers/release/create_addon.go

@@ -0,0 +1,85 @@
+package release
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"github.com/porter-dev/porter/api/server/shared/apierrors"
+	"github.com/porter-dev/porter/api/server/shared/config"
+	"github.com/porter-dev/porter/api/types"
+	"github.com/porter-dev/porter/internal/helm"
+	"github.com/porter-dev/porter/internal/helm/loader"
+	"github.com/porter-dev/porter/internal/models"
+)
+
+type CreateAddonHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+func NewCreateAddonHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *CreateAddonHandler {
+	return &CreateAddonHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+func (c *CreateAddonHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
+	namespace := r.Context().Value(types.NamespaceScope).(string)
+
+	helmAgent, err := c.GetHelmAgent(r, cluster)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	request := &types.CreateAddonRequest{}
+
+	if ok := c.DecodeAndValidate(w, r, request); !ok {
+		return
+	}
+
+	chart, err := loader.LoadChartPublic(request.RepoURL, request.TemplateName, request.TemplateVersion)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	registries, err := c.Repo().Registry().ListRegistriesByProjectID(cluster.ProjectID)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
+		return
+	}
+
+	conf := &helm.InstallChartConfig{
+		Chart:      chart,
+		Name:       request.Name,
+		Namespace:  namespace,
+		Values:     request.Values,
+		Cluster:    cluster,
+		Repo:       c.Repo(),
+		Registries: registries,
+	}
+
+	_, err = helmAgent.InstallChart(conf, c.Config().DOConf)
+
+	if err != nil {
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(
+			fmt.Errorf("error installing a new chart: %s", err.Error()),
+			http.StatusBadRequest,
+		))
+
+		return
+	}
+}

+ 1 - 38
api/server/handlers/release/create_webhook.go

@@ -1,17 +1,14 @@
 package release
 
 import (
-	"fmt"
 	"net/http"
 
 	"github.com/porter-dev/porter/api/server/handlers"
 	"github.com/porter-dev/porter/api/server/shared"
 	"github.com/porter-dev/porter/api/server/shared/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
-	"github.com/porter-dev/porter/api/server/shared/requestutils"
 	"github.com/porter-dev/porter/api/types"
 	"github.com/porter-dev/porter/internal/models"
-	"github.com/porter-dev/porter/internal/repository"
 	"helm.sh/helm/v3/pkg/release"
 )
 
@@ -31,42 +28,8 @@ func NewCreateWebhookHandler(
 func (c *CreateWebhookHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	helmRelease, _ := r.Context().Value(types.ReleaseScope).(*release.Release)
 	cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster)
-	name, _ := requestutils.GetURLParamString(r, types.URLParamReleaseName)
-	namespace := r.Context().Value(types.NamespaceScope).(string)
 
-	token, err := repository.GenerateRandomBytes(16)
-
-	if err != nil {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))
-		return
-	}
-
-	// create release with webhook token in db
-	image, ok := helmRelease.Config["image"].(map[string]interface{})
-
-	if !ok {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("Could not find field image in config")))
-		return
-	}
-
-	repository := image["repository"]
-	repoStr, ok := repository.(string)
-
-	if !ok {
-		c.HandleAPIError(w, r, apierrors.NewErrInternal(fmt.Errorf("Could not find field repository in config")))
-		return
-	}
-
-	release := &models.Release{
-		ClusterID:    cluster.ID,
-		ProjectID:    cluster.ProjectID,
-		Namespace:    namespace,
-		Name:         name,
-		WebhookToken: token,
-		ImageRepoURI: repoStr,
-	}
-
-	release, err = c.Repo().Release().CreateRelease(release)
+	release, err := createReleaseFromHelmRelease(c.Config(), cluster.ProjectID, cluster.ID, helmRelease)
 
 	if err != nil {
 		c.HandleAPIError(w, r, apierrors.NewErrInternal(err))

+ 60 - 0
api/server/router/release.go

@@ -320,5 +320,65 @@ func getReleaseRoutes(
 		Router:   r,
 	})
 
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/releases -> release.NewCreateReleaseHandler
+	createReleaseEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/releases",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+		},
+	)
+
+	createReleaseHandler := release.NewCreateReleaseHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: createReleaseEndpoint,
+		Handler:  createReleaseHandler,
+		Router:   r,
+	})
+
+	// POST /api/projects/{project_id}/clusters/{cluster_id}/namespaces/{namespace}/addons -> release.NewCreateAddonHandler
+	createAddonEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbCreate,
+			Method: types.HTTPVerbPost,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: "/addons",
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+				types.NamespaceScope,
+			},
+		},
+	)
+
+	createAddonHandler := release.NewCreateAddonHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &Route{
+		Endpoint: createAddonEndpoint,
+		Handler:  createAddonHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 }

+ 13 - 1
api/types/git_action_config.go

@@ -15,8 +15,20 @@ type GitActionConfig struct {
 	GitRepoID uint `json:"git_repo_id"`
 
 	// The path to the dockerfile in the git repo
-	DockerfilePath string `json:"dockerfile_path" form:"required"`
+	DockerfilePath string `json:"dockerfile_path"`
 
 	// The build context
 	FolderPath string `json:"folder_path"`
 }
+
+type CreateGitActionConfigRequest struct {
+	GitRepo        string `json:"git_repo" form:"required"`
+	GitBranch      string `json:"git_branch"`
+	ImageRepoURI   string `json:"image_repo_uri" form:"required"`
+	DockerfilePath string `json:"dockerfile_path"`
+	FolderPath     string `json:"folder_path"`
+	GitRepoID      uint   `json:"git_repo_id" form:"required"`
+	RegistryID     uint   `json:"registry_id"`
+
+	ShouldCreateWorkflow bool `json:"should_create_workflow"`
+}

+ 19 - 0
api/types/release.go

@@ -25,3 +25,22 @@ type UpdateNotificationConfigRequest struct {
 		Failure bool `json:"failure"`
 	} `json:"payload"`
 }
+
+type CreateReleaseBaseRequest struct {
+	RepoURL         string                 `schema:"repo_url"`
+	TemplateName    string                 `json:"template_name" form:"required"`
+	TemplateVersion string                 `json:"template_version" form:"required"`
+	Values          map[string]interface{} `json:"values"`
+	Name            string                 `json:"name" form:"required"`
+}
+
+type CreateReleaseRequest struct {
+	*CreateReleaseBaseRequest
+
+	ImageURL           string                        `json:"image_url" form:"required"`
+	GithubActionConfig *CreateGitActionConfigRequest `json:"github_action_config,omitempty"`
+}
+
+type CreateAddonRequest struct {
+	*CreateReleaseBaseRequest
+}

+ 11 - 15
dashboard/src/main/home/launch/launch-flow/LaunchFlow.tsx

@@ -100,17 +100,15 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
       .deployAddon(
         "<token>",
         {
-          templateName: props.currentTemplate.name,
-          storage: StorageType.Secret,
-          formValues: values,
-          namespace: selectedNamespace,
-          name: templateName,
+          template_name: props.currentTemplate.name,
+          template_version: props.currentTemplate?.currentVersion || "latest",
+          values: values,
+          name: props.currentTemplate.name.toLowerCase().trim(),
         },
         {
           id: currentProject.id,
           cluster_id: currentCluster.id,
-          name: props.currentTemplate.name.toLowerCase().trim(),
-          version: props.currentTemplate?.currentVersion || "latest",
+          namespace: selectedNamespace,
           repo_url: process.env.ADDON_CHART_REPO_URL,
         }
       )
@@ -249,19 +247,17 @@ const LaunchFlow: React.FC<PropsType> = (props) => {
       .deployTemplate(
         "<token>",
         {
-          templateName: props.currentTemplate.name,
-          imageURL: url,
-          storage: StorageType.Secret,
-          formValues: values,
-          namespace: selectedNamespace,
+          image_url: url,
+          values: values,
+          template_name: props.currentTemplate.name.toLowerCase().trim(),
+          template_version: props.currentTemplate?.currentVersion || "latest",
           name: templateName,
-          githubActionConfig,
+          github_action_config: githubActionConfig,
         },
         {
           id: currentProject.id,
           cluster_id: currentCluster.id,
-          name: props.currentTemplate.name.toLowerCase().trim(),
-          version: props.currentTemplate?.currentVersion || "latest",
+          namespace: selectedNamespace,
           repo_url: process.env.APPLICATION_CHART_REPO_URL,
         }
       )

+ 15 - 19
dashboard/src/shared/api.tsx

@@ -292,49 +292,45 @@ const generateGHAWorkflow = baseApi<
 
 const deployTemplate = baseApi<
   {
-    templateName: string;
-    imageURL?: string;
-    formValues?: any;
-    storage: StorageType;
-    namespace: string;
+    template_name: string;
+    template_version: string;
+    image_url?: string;
+    values?: any;
     name: string;
-    githubActionConfig?: FullActionConfigType;
+    github_action_config?: FullActionConfigType;
   },
   {
     id: number;
     cluster_id: number;
-    name: string;
-    version: string;
+    namespace: string;
     repo_url?: string;
   }
 >("POST", (pathParams) => {
-  let { cluster_id, id, name, version, repo_url } = pathParams;
+  let { cluster_id, id, namespace, repo_url } = pathParams;
 
   if (repo_url) {
-    return `/api/projects/${id}/deploy/${name}/${version}?cluster_id=${cluster_id}&repo_url=${repo_url}`;
+    return `/api/projects/${id}/clusters/${cluster_id}/namespaces/${namespace}/releases?repo_url=${repo_url}`;
   }
-  return `/api/projects/${id}/deploy/${name}/${version}?cluster_id=${cluster_id}`;
+  return `/api/projects/${id}/clusters/${cluster_id}/namespaces/${namespace}/releases`;
 });
 
 const deployAddon = baseApi<
   {
-    templateName: string;
-    formValues?: any;
-    storage: StorageType;
-    namespace: string;
+    template_name: string;
+    template_version: string;
+    values?: any;
     name: string;
   },
   {
     id: number;
     cluster_id: number;
-    name: string;
-    version: string;
+    namespace: string;
     repo_url?: string;
   }
 >("POST", (pathParams) => {
-  let { cluster_id, id, name, version, repo_url } = pathParams;
+  let { cluster_id, id, namespace, repo_url } = pathParams;
 
-  return `/api/projects/${id}/deploy/addon/${name}/${version}?cluster_id=${cluster_id}&repo_url=${repo_url}`;
+  return `/api/projects/${id}/clusters/${cluster_id}/namespaces/${namespace}/addons?repo_url=${repo_url}`;
 });
 
 const destroyCluster = baseApi<

+ 2 - 2
docs/developing/backend-refactor-status.md

@@ -48,8 +48,8 @@
 | <li>- [ ] `GET /api/projects/{project_id}/clusters/{cluster_id}/nodes`                                                      |             |                 |             |                  |
 | <li>- [ ] `GET /api/projects/{project_id}/collaborators`                                                                    |             |                 |             |                  |
 | <li>- [ ] `POST /api/projects/{project_id}/delete/{name}`                                                                   |             |                 |             |                  |
-| <li>- [ ] `POST /api/projects/{project_id}/deploy/addon/{name}/{version}`                                                   |             |                 |             |                  |
-| <li>- [ ] `POST /api/projects/{project_id}/deploy/{name}/{version}`                                                         |             |                 |             |                  |
+| <li>- [X] `POST /api/projects/{project_id}/deploy/addon/{name}/{version}`                                                   | AB          |                 |             |                  |
+| <li>- [X] `POST /api/projects/{project_id}/deploy/{name}/{version}`                                                         | AB          |                 |             |                  |
 | <li>- [X] `GET /api/projects/{project_id}/gitrepos`                                                                         | AB          |                 |             |                  |
 | <li>- [X] `GET /api/projects/{project_id}/gitrepos/{installation_id}/repos`                                                 | AB          |                 |             |                  |
 | <li>- [X] `GET /api/projects/{project_id}/gitrepos/{installation_id}/repos/{kind}/{owner}/{name}/branches`                  | AB          |                 |             |                  |