diff --git a/api/client/deployment_target.go b/api/client/deployment_target.go new file mode 100644 index 0000000000..0a0d7b2ba3 --- /dev/null +++ b/api/client/deployment_target.go @@ -0,0 +1,34 @@ +package client + +import ( + "context" + "fmt" + + "github.com/porter-dev/porter/api/server/handlers/deployment_target" +) + +// CreateDeploymentTarget creates a new deployment target for a given project and cluster with the provided name +func (c *Client) CreateDeploymentTarget( + ctx context.Context, + projectID, clusterID uint, + selector string, + preview bool, +) (*deployment_target.CreateDeploymentTargetResponse, error) { + resp := &deployment_target.CreateDeploymentTargetResponse{} + + req := &deployment_target.CreateDeploymentTargetRequest{ + Selector: selector, + Preview: preview, + } + + err := c.postRequest( + fmt.Sprintf( + "/projects/%d/clusters/%d/deployment-targets", + projectID, clusterID, + ), + req, + resp, + ) + + return resp, err +} diff --git a/api/server/handlers/deployment_target/create.go b/api/server/handlers/deployment_target/create.go new file mode 100644 index 0000000000..2bb4d49465 --- /dev/null +++ b/api/server/handlers/deployment_target/create.go @@ -0,0 +1,115 @@ +package deployment_target + +import ( + "net/http" + + "github.com/google/uuid" + "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/models" + "github.com/porter-dev/porter/internal/telemetry" +) + +// CreateDeploymentTargetHandler is the handler for the /deployment-targets endpoint +type CreateDeploymentTargetHandler struct { + handlers.PorterHandlerReadWriter +} + +// NewCreateDeploymentTargetHandler handles POST requests to the endpoint /deployment-targets +func NewCreateDeploymentTargetHandler( + config *config.Config, + decoderValidator shared.RequestDecoderValidator, + writer shared.ResultWriter, +) *CreateDeploymentTargetHandler { + return &CreateDeploymentTargetHandler{ + PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer), + } +} + +// CreateDeploymentTargetRequest is the request object for the /deployment-targets POST endpoint +type CreateDeploymentTargetRequest struct { + Selector string `json:"selector"` + Preview bool `json:"preview"` +} + +// CreateDeploymentTargetResponse is the response object for the /deployment-targets POST endpoint +type CreateDeploymentTargetResponse struct { + DeploymentTargetID string `json:"deployment_target_id"` +} + +// ServeHTTP handles POST requests to create a new deployment target +func (c *CreateDeploymentTargetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx, span := telemetry.NewSpan(r.Context(), "serve-create-deployment-target") + defer span.End() + + project, _ := ctx.Value(types.ProjectScope).(*models.Project) + cluster, _ := ctx.Value(types.ClusterScope).(*models.Cluster) + + if !project.GetFeatureFlag(models.ValidateApplyV2, c.Config().LaunchDarklyClient) { + err := telemetry.Error(ctx, span, nil, "project does not have validate apply v2 enabled") + c.HandleAPIError(w, r, apierrors.NewErrForbidden(err)) + return + } + + request := &CreateDeploymentTargetRequest{} + if ok := c.DecodeAndValidate(w, r, request); !ok { + err := telemetry.Error(ctx, span, nil, "error decoding request") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + if request.Selector == "" { + err := telemetry.Error(ctx, span, nil, "name is required") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) + return + } + + var res *CreateDeploymentTargetResponse + + existingDeploymentTarget, err := c.Repo().DeploymentTarget().DeploymentTargetBySelectorAndSelectorType( + project.ID, + cluster.ID, + request.Selector, + string(models.DeploymentTargetSelectorType_Namespace), + ) + if err != nil { + err := telemetry.Error(ctx, span, err, "error checking for existing deployment target") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + if existingDeploymentTarget.ID != uuid.Nil { + res = &CreateDeploymentTargetResponse{ + DeploymentTargetID: existingDeploymentTarget.ID.String(), + } + c.WriteResult(w, r, res) + return + } + + deploymentTarget := &models.DeploymentTarget{ + ProjectID: int(project.ID), + ClusterID: int(cluster.ID), + Selector: request.Selector, + SelectorType: models.DeploymentTargetSelectorType_Namespace, + Preview: request.Preview, + } + deploymentTarget, err = c.Repo().DeploymentTarget().CreateDeploymentTarget(deploymentTarget) + if err != nil { + err := telemetry.Error(ctx, span, err, "error creating deployment target") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + if deploymentTarget.ID == uuid.Nil { + err := telemetry.Error(ctx, span, nil, "deployment target id is nil") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + + res = &CreateDeploymentTargetResponse{ + DeploymentTargetID: deploymentTarget.ID.String(), + } + + c.WriteResult(w, r, res) +} diff --git a/api/server/handlers/porter_app/apply.go b/api/server/handlers/porter_app/apply.go index d2b8e1cf13..b2f22d9685 100644 --- a/api/server/handlers/porter_app/apply.go +++ b/api/server/handlers/porter_app/apply.go @@ -13,6 +13,7 @@ import ( "github.com/porter-dev/api-contracts/generated/go/helpers" + "github.com/porter-dev/porter/internal/deployment_target" "github.com/porter-dev/porter/internal/porter_app" "github.com/porter-dev/porter/internal/telemetry" @@ -124,6 +125,18 @@ func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request telemetry.AttributeKV{Key: "deployment-target-id", Value: request.DeploymentTargetId}, ) + deploymentTargetDetails, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{ + ProjectID: int64(project.ID), + ClusterID: int64(cluster.ID), + DeploymentTargetID: deploymentTargetID, + CCPClient: c.Config().ClusterControlPlaneClient, + }) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting deployment target details") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + agent, err := c.GetAgent(r, cluster, "") if err != nil { err := telemetry.Error(ctx, span, err, "error getting kubernetes agent") @@ -139,7 +152,7 @@ func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request KubernetesAgent: agent, } - appProto, err = addPorterSubdomainsIfNecessary(ctx, appProto, subdomainCreateInput) + appProto, err = addPorterSubdomainsIfNecessary(ctx, appProto, deploymentTargetDetails, subdomainCreateInput) if err != nil { err := telemetry.Error(ctx, span, err, "error adding porter subdomains") c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) @@ -197,7 +210,7 @@ func (c *ApplyPorterAppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request } // addPorterSubdomainsIfNecessary adds porter subdomains to the app proto if a web service is changed to private and has no domains -func addPorterSubdomainsIfNecessary(ctx context.Context, app *porterv1.PorterApp, createSubdomainInput porter_app.CreatePorterSubdomainInput) (*porterv1.PorterApp, error) { +func addPorterSubdomainsIfNecessary(ctx context.Context, app *porterv1.PorterApp, deploymentTarget deployment_target.DeploymentTarget, createSubdomainInput porter_app.CreatePorterSubdomainInput) (*porterv1.PorterApp, error) { for serviceName, service := range app.Services { if service.Type == porterv1.ServiceType_SERVICE_TYPE_WEB { if service.GetWebConfig() == nil { @@ -207,6 +220,10 @@ func addPorterSubdomainsIfNecessary(ctx context.Context, app *porterv1.PorterApp webConfig := service.GetWebConfig() if !webConfig.GetPrivate() && len(webConfig.Domains) == 0 { + if deploymentTarget.Namespace != DeploymentTargetSelector_Default { + createSubdomainInput.AppName = fmt.Sprintf("%s-%s", createSubdomainInput.AppName, deploymentTarget.Namespace) + } + subdomain, err := porter_app.CreatePorterSubdomain(ctx, createSubdomainInput) if err != nil { return app, fmt.Errorf("error creating subdomain: %w", err) diff --git a/api/server/handlers/porter_app/get_app_env.go b/api/server/handlers/porter_app/get_app_env.go index f647452969..be24bea450 100644 --- a/api/server/handlers/porter_app/get_app_env.go +++ b/api/server/handlers/porter_app/get_app_env.go @@ -14,6 +14,7 @@ import ( "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/deployment_target" "github.com/porter-dev/porter/internal/kubernetes/environment_groups" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/porter_app" @@ -124,12 +125,24 @@ func (c *GetAppEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{ + ProjectID: int64(project.ID), + ClusterID: int64(cluster.ID), + DeploymentTargetID: revision.DeploymentTargetID, + CCPClient: c.Config().ClusterControlPlaneClient, + }) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting deployment target details") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + envFromProtoInp := porter_app.AppEnvironmentFromProtoInput{ - ProjectID: project.ID, - ClusterID: int(cluster.ID), - App: appProto, - K8SAgent: agent, - DeploymentTargetRepository: c.Repo().DeploymentTarget(), + ProjectID: project.ID, + ClusterID: int(cluster.ID), + DeploymentTarget: deploymentTarget, + App: appProto, + K8SAgent: agent, } envGroups, err := porter_app.AppEnvironmentFromProto(ctx, envFromProtoInp, porter_app.WithEnvGroupFilter(request.EnvGroups), porter_app.WithSecrets(), porter_app.WithoutDefaultAppEnvGroups()) @@ -140,13 +153,12 @@ func (c *GetAppEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{ - ProjectID: project.ID, - ClusterID: int(cluster.ID), - DeploymentTargetID: revision.DeploymentTargetID, - Revision: revision, - K8SAgent: agent, - PorterAppRepository: c.Repo().PorterApp(), - DeploymentTargetRepository: c.Repo().DeploymentTarget(), + ProjectID: project.ID, + ClusterID: int(cluster.ID), + Revision: revision, + DeploymentTarget: deploymentTarget, + K8SAgent: agent, + PorterAppRepository: c.Repo().PorterApp(), }) if err != nil { err := telemetry.Error(ctx, span, err, "error attaching env to revision") diff --git a/api/server/handlers/porter_app/get_app_revision.go b/api/server/handlers/porter_app/get_app_revision.go index 868a7ada05..b34c23918e 100644 --- a/api/server/handlers/porter_app/get_app_revision.go +++ b/api/server/handlers/porter_app/get_app_revision.go @@ -12,6 +12,7 @@ import ( "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/deployment_target" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/porter_app" "github.com/porter-dev/porter/internal/telemetry" @@ -86,14 +87,25 @@ func (c *GetAppRevisionHandler) ServeHTTP(w http.ResponseWriter, r *http.Request return } + deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{ + ProjectID: int64(project.ID), + ClusterID: int64(cluster.ID), + DeploymentTargetID: ccpResp.Msg.AppRevision.DeploymentTargetId, + CCPClient: c.Config().ClusterControlPlaneClient, + }) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting deployment target details") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + revisionWithEnv, err := porter_app.AttachEnvToRevision(ctx, porter_app.AttachEnvToRevisionInput{ - ProjectID: project.ID, - ClusterID: int(cluster.ID), - Revision: encodedRevision, - DeploymentTargetID: ccpResp.Msg.AppRevision.DeploymentTargetId, - K8SAgent: agent, - PorterAppRepository: c.Repo().PorterApp(), - DeploymentTargetRepository: c.Repo().DeploymentTarget(), + ProjectID: project.ID, + ClusterID: int(cluster.ID), + Revision: encodedRevision, + DeploymentTarget: deploymentTarget, + K8SAgent: agent, + PorterAppRepository: c.Repo().PorterApp(), }) if err != nil { err := telemetry.Error(ctx, span, err, "error attaching env to revision") diff --git a/api/server/handlers/porter_app/get_build_env.go b/api/server/handlers/porter_app/get_build_env.go index 6dec280c6e..d4d9315d9f 100644 --- a/api/server/handlers/porter_app/get_build_env.go +++ b/api/server/handlers/porter_app/get_build_env.go @@ -14,6 +14,7 @@ import ( "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/deployment_target" "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/porter_app" "github.com/porter-dev/porter/internal/telemetry" @@ -114,12 +115,24 @@ func (c *GetBuildEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + deploymentTarget, err := deployment_target.DeploymentTargetDetails(ctx, deployment_target.DeploymentTargetDetailsInput{ + ProjectID: int64(project.ID), + ClusterID: int64(cluster.ID), + DeploymentTargetID: revision.DeploymentTargetID, + CCPClient: c.Config().ClusterControlPlaneClient, + }) + if err != nil { + err := telemetry.Error(ctx, span, err, "error getting deployment target details") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) + return + } + envFromProtoInp := porter_app.AppEnvironmentFromProtoInput{ - ProjectID: project.ID, - ClusterID: int(cluster.ID), - App: appProto, - K8SAgent: agent, - DeploymentTargetRepository: c.Repo().DeploymentTarget(), + ProjectID: project.ID, + ClusterID: int(cluster.ID), + DeploymentTarget: deploymentTarget, + App: appProto, + K8SAgent: agent, } envGroups, err := porter_app.AppEnvironmentFromProto(ctx, envFromProtoInp) if err != nil { diff --git a/api/server/handlers/porter_app/latest_app_revisions.go b/api/server/handlers/porter_app/latest_app_revisions.go index b65d57365a..a6ddd22363 100644 --- a/api/server/handlers/porter_app/latest_app_revisions.go +++ b/api/server/handlers/porter_app/latest_app_revisions.go @@ -4,6 +4,7 @@ import ( "net/http" "connectrpc.com/connect" + "github.com/google/uuid" porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1" "github.com/porter-dev/porter/api/server/handlers" "github.com/porter-dev/porter/api/server/shared" @@ -52,34 +53,22 @@ func (c *LatestAppRevisionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Req project, _ := r.Context().Value(types.ProjectScope).(*models.Project) cluster, _ := r.Context().Value(types.ClusterScope).(*models.Cluster) - deploymentTargets, err := c.Repo().DeploymentTarget().List(project.ID) + // todo(ianedwards): once we have a way to select a deployment target, we can add it to the request + defaultDeploymentTarget, err := c.Repo().DeploymentTarget().DeploymentTargetBySelectorAndSelectorType(project.ID, cluster.ID, DeploymentTargetSelector_Default, DeploymentTargetSelectorType_Default) if err != nil { - err = telemetry.Error(ctx, span, err, "error reading deployment targets") - c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) - return - } - - if len(deploymentTargets) == 0 { - res := &LatestAppRevisionsResponse{ - AppRevisions: []LatestRevisionWithSource{}, - } - - c.WriteResult(w, r, res) + err := telemetry.Error(ctx, span, err, "error getting default deployment target from repo") + c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest)) return } - - if len(deploymentTargets) > 1 { - err = telemetry.Error(ctx, span, err, "more than one deployment target found") + if defaultDeploymentTarget.ID == uuid.Nil { + err := telemetry.Error(ctx, span, err, "default deployment target not found") c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError)) return } - // todo(ianedwards): once we have a way to select a deployment target, we can add it to the request - deploymentTarget := deploymentTargets[0] - listAppRevisionsReq := connect.NewRequest(&porterv1.LatestAppRevisionsRequest{ ProjectId: int64(project.ID), - DeploymentTargetId: deploymentTarget.ID.String(), + DeploymentTargetId: defaultDeploymentTarget.ID.String(), }) latestAppRevisionsResp, err := c.Config().ClusterControlPlaneClient.LatestAppRevisions(ctx, listAppRevisionsReq) diff --git a/api/server/router/deployment_target.go b/api/server/router/deployment_target.go new file mode 100644 index 0000000000..bfc063ac57 --- /dev/null +++ b/api/server/router/deployment_target.go @@ -0,0 +1,89 @@ +package router + +import ( + "github.com/go-chi/chi/v5" + "github.com/porter-dev/porter/api/server/handlers/deployment_target" + "github.com/porter-dev/porter/api/server/shared" + "github.com/porter-dev/porter/api/server/shared/config" + "github.com/porter-dev/porter/api/server/shared/router" + "github.com/porter-dev/porter/api/types" +) + +// NewDeploymentTargetScopedRegisterer applies /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets routes to the gin Router +func NewDeploymentTargetScopedRegisterer(children ...*router.Registerer) *router.Registerer { + return &router.Registerer{ + GetRoutes: GetDeploymentTargetScopedRoutes, + Children: children, + } +} + +// GetDeploymentTargetScopedRoutes returns the router handlers specific to deployment targets +func GetDeploymentTargetScopedRoutes( + r chi.Router, + config *config.Config, + basePath *types.Path, + factory shared.APIEndpointFactory, + children ...*router.Registerer, +) []*router.Route { + routes, projPath := getDeploymentTargetRoutes(r, config, basePath, factory) + + if len(children) > 0 { + r.Route(projPath.RelativePath, func(r chi.Router) { + for _, child := range children { + childRoutes := child.GetRoutes(r, config, basePath, factory, child.Children...) + + routes = append(routes, childRoutes...) + } + }) + } + + return routes +} + +// getDeploymentTargetRoutes gets the routes specific to deployment targets +func getDeploymentTargetRoutes( + r chi.Router, + config *config.Config, + basePath *types.Path, + factory shared.APIEndpointFactory, +) ([]*router.Route, *types.Path) { + relPath := "/deployment-targets" + + newPath := &types.Path{ + Parent: basePath, + RelativePath: relPath, + } + + var routes []*router.Route + + // POST /api/projects/{project_id}/clusters/{cluster_id}/deployment-targets -> deployment_target.CreateDeploymentTargetHandler + createDeploymentTargetEndpoint := factory.NewAPIEndpoint( + &types.APIRequestMetadata{ + Verb: types.APIVerbCreate, + Method: types.HTTPVerbPost, + Path: &types.Path{ + Parent: basePath, + RelativePath: relPath, + }, + Scopes: []types.PermissionScope{ + types.UserScope, + types.ProjectScope, + types.ClusterScope, + }, + }, + ) + + createDeploymentTargetHandler := deployment_target.NewCreateDeploymentTargetHandler( + config, + factory.GetDecoderValidator(), + factory.GetResultWriter(), + ) + + routes = append(routes, &router.Route{ + Endpoint: createDeploymentTargetEndpoint, + Handler: createDeploymentTargetHandler, + Router: r, + }) + + return routes, newPath +} diff --git a/api/server/router/router.go b/api/server/router/router.go index 4a11e45366..cffc18179b 100644 --- a/api/server/router/router.go +++ b/api/server/router/router.go @@ -32,7 +32,8 @@ func NewAPIRouter(config *config.Config) *chi.Mux { namespaceRegisterer := NewNamespaceScopedRegisterer(releaseRegisterer) clusterIntegrationRegisterer := NewClusterIntegrationScopedRegisterer() stackRegisterer := NewPorterAppScopedRegisterer() - clusterRegisterer := NewClusterScopedRegisterer(namespaceRegisterer, clusterIntegrationRegisterer, stackRegisterer) + deploymentTargetRegisterer := NewDeploymentTargetScopedRegisterer() + clusterRegisterer := NewClusterScopedRegisterer(namespaceRegisterer, clusterIntegrationRegisterer, stackRegisterer, deploymentTargetRegisterer) infraRegisterer := NewInfraScopedRegisterer() gitInstallationRegisterer := NewGitInstallationScopedRegisterer() registryRegisterer := NewRegistryScopedRegisterer() diff --git a/cli/cmd/commands/apply.go b/cli/cmd/commands/apply.go index 5c27a2ebba..cbff98159e 100644 --- a/cli/cmd/commands/apply.go +++ b/cli/cmd/commands/apply.go @@ -39,7 +39,10 @@ import ( "gopkg.in/yaml.v2" ) -var porterYAML string +var ( + porterYAML string + previewApply bool +) func registerCommand_Apply(cliConf config.CLIConfig) *cobra.Command { applyCmd := &cobra.Command{ @@ -103,6 +106,7 @@ applying a configuration: applyCmd.AddCommand(applyValidateCmd) applyCmd.PersistentFlags().StringVarP(&porterYAML, "file", "f", "", "path to porter.yaml") + applyCmd.PersistentFlags().BoolVarP(&previewApply, "preview", "p", false, "apply as preview environment based on current git branch") applyCmd.MarkFlagRequired("file") return applyCmd @@ -122,7 +126,18 @@ func apply(ctx context.Context, _ *types.GetAuthenticatedUserResponse, client ap } if project.ValidateApplyV2 { - err = v2.Apply(ctx, cliConfig, client, porterYAML, appName) + if previewApply && !project.PreviewEnvsEnabled { + return fmt.Errorf("preview environments are not enabled for this project. Please contact support@porter.run") + } + + inp := v2.ApplyInput{ + CLIConfig: cliConfig, + Client: client, + PorterYamlPath: porterYAML, + AppName: appName, + PreviewApply: previewApply, + } + err = v2.Apply(ctx, inp) if err != nil { return err } diff --git a/cli/cmd/v2/apply.go b/cli/cmd/v2/apply.go index b8f756ad83..a38b228795 100644 --- a/cli/cmd/v2/apply.go +++ b/cli/cmd/v2/apply.go @@ -24,27 +24,40 @@ import ( "github.com/porter-dev/porter/cli/cmd/config" ) +// ApplyInput is the input for the Apply function +type ApplyInput struct { + // CLIConfig is the CLI configuration + CLIConfig config.CLIConfig + // Client is the Porter API client + Client api.Client + // PorterYamlPath is the path to the porter.yaml file + PorterYamlPath string + // AppName is the name of the app + AppName string + // PreviewApply is true when Apply should create a new deployment target matching current git branch and apply to that target + PreviewApply bool +} + // Apply implements the functionality of the `porter apply` command for validate apply v2 projects -func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, porterYamlPath string, appName string) error { +func Apply(ctx context.Context, inp ApplyInput) error { const forceBuild = true var b64AppProto string - targetResp, err := client.DefaultDeploymentTarget(ctx, cliConf.Project, cliConf.Cluster) - if err != nil { - return fmt.Errorf("error calling default deployment target endpoint: %w", err) - } + cliConf := inp.CLIConfig + client := inp.Client - if targetResp.DeploymentTargetID == "" { - return errors.New("deployment target id is empty") + deploymentTargetID, err := deploymentTargetFromConfig(ctx, client, cliConf.Project, cliConf.Cluster, inp.PreviewApply) + if err != nil { + return fmt.Errorf("error getting deployment target from config: %w", err) } - porterYamlExists := len(porterYamlPath) != 0 + porterYamlExists := len(inp.PorterYamlPath) != 0 if porterYamlExists { - _, err = os.Stat(filepath.Clean(porterYamlPath)) + _, err := os.Stat(filepath.Clean(inp.PorterYamlPath)) if err != nil { if !os.IsNotExist(err) { - return fmt.Errorf("error checking if porter yaml exists at path %s: %w", porterYamlPath, err) + return fmt.Errorf("error checking if porter yaml exists at path %s: %w", inp.PorterYamlPath, err) } // If a path was specified but the file does not exist, we will not immediately error out. // This supports users migrated from v1 who use a workflow file that always specifies a porter yaml path @@ -53,8 +66,9 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por } } + appName := inp.AppName if porterYamlExists { - porterYaml, err := os.ReadFile(filepath.Clean(porterYamlPath)) + porterYaml, err := os.ReadFile(filepath.Clean(inp.PorterYamlPath)) if err != nil { return fmt.Errorf("could not read porter yaml file: %w", err) } @@ -75,7 +89,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por // override app name if provided appName, err = appNameFromB64AppProto(parseResp.B64AppProto) if err != nil { - return fmt.Errorf("error getting app name from b64 app proto: %w", err) + return fmt.Errorf("error getting app name from porter.yaml: %w", err) } // we only need to create the app if a porter yaml is provided (otherwise it must already exist) @@ -92,7 +106,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por return fmt.Errorf("unable to create porter app from yaml: %w", err) } - envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, targetResp.DeploymentTargetID, parseResp.EnvVariables, parseResp.EnvSecrets, parseResp.B64AppProto) + envGroupResp, err := client.CreateOrUpdateAppEnvironment(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID, parseResp.EnvVariables, parseResp.EnvSecrets, parseResp.B64AppProto) if err != nil { return fmt.Errorf("error calling create or update app environment group endpoint: %w", err) } @@ -109,16 +123,9 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por return errors.New("App name is empty. Please provide a Porter YAML file specifying the name of the app or set the PORTER_APP_NAME environment variable.") } - var commitSHA string - if os.Getenv("PORTER_COMMIT_SHA") != "" { - commitSHA = os.Getenv("PORTER_COMMIT_SHA") - } else if os.Getenv("GITHUB_SHA") != "" { - commitSHA = os.Getenv("GITHUB_SHA") - } else if commit, err := git.LastCommit(); err == nil && commit != nil { - commitSHA = commit.Sha - } + commitSHA := commitSHAFromEnv() - validateResp, err := client.ValidatePorterApp(ctx, cliConf.Project, cliConf.Cluster, appName, b64AppProto, targetResp.DeploymentTargetID, commitSHA) + validateResp, err := client.ValidatePorterApp(ctx, cliConf.Project, cliConf.Cluster, appName, b64AppProto, deploymentTargetID, commitSHA) if err != nil { return fmt.Errorf("error calling validate endpoint: %w", err) } @@ -128,7 +135,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por } base64AppProto := validateResp.ValidatedBase64AppProto - applyResp, err := client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, base64AppProto, targetResp.DeploymentTargetID, "", forceBuild) + applyResp, err := client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, base64AppProto, deploymentTargetID, "", forceBuild) if err != nil { return fmt.Errorf("error calling apply endpoint: %w", err) } @@ -140,39 +147,39 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por if applyResp.CLIAction == porterv1.EnumCLIAction_ENUM_CLI_ACTION_BUILD { color.New(color.FgGreen).Printf("Building new image...\n") // nolint:errcheck,gosec - eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID) + eventID, _ := createBuildEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID) if commitSHA == "" { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return errors.New("Build is required but commit SHA cannot be identified. Please set the PORTER_COMMIT_SHA environment variable or run apply in git repository with access to the git CLI.") } buildSettings, err := buildSettingsFromBase64AppProto(base64AppProto) if err != nil { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return fmt.Errorf("error building settings from base64 app proto: %w", err) } - currentAppRevisionResp, err := client.CurrentAppRevision(ctx, cliConf.Project, cliConf.Cluster, appName, targetResp.DeploymentTargetID) + currentAppRevisionResp, err := client.CurrentAppRevision(ctx, cliConf.Project, cliConf.Cluster, appName, deploymentTargetID) if err != nil { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return fmt.Errorf("error getting current app revision: %w", err) } if currentAppRevisionResp == nil { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return errors.New("current app revision is nil") } appRevision := currentAppRevisionResp.AppRevision if appRevision.B64AppProto == "" { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return errors.New("current app revision b64 app proto is empty") } currentImageTag, err := imageTagFromBase64AppProto(appRevision.B64AppProto) if err != nil { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return fmt.Errorf("error getting image tag from current app revision: %w", err) } @@ -181,14 +188,14 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por buildEnv, err := client.GetBuildEnv(ctx, cliConf.Project, cliConf.Cluster, appName, appRevision.ID) if err != nil { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return fmt.Errorf("error getting build env: %w", err) } buildSettings.Env = buildEnv.BuildEnvVariables err = build(ctx, client, buildSettings) if err != nil { - _ = reportBuildFailure(ctx, client, appName, cliConf, targetResp.DeploymentTargetID, applyResp.AppRevisionId, eventID) + _ = reportBuildFailure(ctx, client, appName, cliConf, deploymentTargetID, applyResp.AppRevisionId, eventID) return fmt.Errorf("error building app: %w", err) } @@ -196,7 +203,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por buildMetadata := make(map[string]interface{}) buildMetadata["end_time"] = time.Now().UTC() - _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, eventID, types.PorterAppEventStatus_Success, buildMetadata) + _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, eventID, types.PorterAppEventStatus_Success, buildMetadata) applyResp, err = client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, "", "", applyResp.AppRevisionId, !forceBuild) if err != nil { @@ -210,7 +217,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por color.New(color.FgGreen).Printf("Waiting for predeploy to complete...\n") // nolint:errcheck,gosec now := time.Now().UTC() - eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, now, applyResp.AppRevisionId) + eventID, _ := createPredeployEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, now, applyResp.AppRevisionId) eventStatus := types.PorterAppEventStatus_Success for { @@ -236,7 +243,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por metadata := make(map[string]interface{}) metadata["end_time"] = time.Now().UTC() - _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, targetResp.DeploymentTargetID, eventID, eventStatus, metadata) + _ = updateExistingEvent(ctx, client, appName, cliConf.Project, cliConf.Cluster, deploymentTargetID, eventID, eventStatus, metadata) applyResp, err = client.ApplyPorterApp(ctx, cliConf.Project, cliConf.Cluster, "", "", applyResp.AppRevisionId, !forceBuild) if err != nil { @@ -252,6 +259,19 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por return nil } +func commitSHAFromEnv() string { + var commitSHA string + if os.Getenv("PORTER_COMMIT_SHA") != "" { + commitSHA = os.Getenv("PORTER_COMMIT_SHA") + } else if os.Getenv("GITHUB_SHA") != "" { + commitSHA = os.Getenv("GITHUB_SHA") + } else if commit, err := git.LastCommit(); err == nil && commit != nil { + commitSHA = commit.Sha + } + + return commitSHA +} + // checkPredeployTimeout is the maximum amount of time the CLI will wait for a predeploy to complete before calling apply again const checkPredeployTimeout = 60 * time.Minute @@ -358,6 +378,41 @@ func buildSettingsFromBase64AppProto(base64AppProto string) (buildInput, error) }, nil } +func deploymentTargetFromConfig(ctx context.Context, client api.Client, projectID, clusterID uint, previewApply bool) (string, error) { + var deploymentTargetID string + + targetResp, err := client.DefaultDeploymentTarget(ctx, projectID, clusterID) + if err != nil { + return deploymentTargetID, fmt.Errorf("error calling default deployment target endpoint: %w", err) + } + deploymentTargetID = targetResp.DeploymentTargetID + + if previewApply { + var branchName string + if os.Getenv("GITHUB_REF_NAME") != "" { + branchName = os.Getenv("GITHUB_REF_NAME") + } else if branch, err := git.CurrentBranch(); err == nil { + branchName = branch + } + + if branchName == "" { + return deploymentTargetID, errors.New("Branch name is empty. Please run apply in a git repository with access to the git CLI.") + } + + targetResp, err := client.CreateDeploymentTarget(ctx, projectID, clusterID, branchName, true) + if err != nil { + return deploymentTargetID, fmt.Errorf("error calling create deployment target endpoint: %w", err) + } + deploymentTargetID = targetResp.DeploymentTargetID + } + + if deploymentTargetID == "" { + return deploymentTargetID, errors.New("deployment target id is empty") + } + + return deploymentTargetID, nil +} + func imageTagFromBase64AppProto(base64AppProto string) (string, error) { var image string diff --git a/cli/cmd/v2/deploy.go b/cli/cmd/v2/deploy.go index f67fff813b..bb38a5cbe0 100644 --- a/cli/cmd/v2/deploy.go +++ b/cli/cmd/v2/deploy.go @@ -13,7 +13,14 @@ func UpdateFull(ctx context.Context, cliConf config.CLIConfig, client api.Client // use empty string for porterYamlPath,legacy projects wont't have a v2 porter.yaml var porterYamlPath string - err := Apply(ctx, cliConf, client, porterYamlPath, appName) + inp := ApplyInput{ + CLIConfig: cliConf, + Client: client, + PorterYamlPath: porterYamlPath, + AppName: appName, + } + + err := Apply(ctx, inp) if err != nil { return err } diff --git a/internal/deployment_target/get.go b/internal/deployment_target/get.go new file mode 100644 index 0000000000..2d7d65082f --- /dev/null +++ b/internal/deployment_target/get.go @@ -0,0 +1,70 @@ +package deployment_target + +import ( + "context" + + "connectrpc.com/connect" + 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/telemetry" +) + +// DeploymentTargetDetailsInput is the input to the DeploymentTargetDetails function +type DeploymentTargetDetailsInput struct { + ProjectID int64 + ClusterID int64 + DeploymentTargetID string + CCPClient porterv1connect.ClusterControlPlaneServiceClient +} + +// DeploymentTarget is a struct representing the unique cluster, namespace pair for a deployment target +type DeploymentTarget struct { + ClusterID int64 `json:"cluster_id"` + Namespace string `json:"namespace"` +} + +// DeploymentTargetDetails gets the deployment target details from CCP +func DeploymentTargetDetails(ctx context.Context, inp DeploymentTargetDetailsInput) (DeploymentTarget, error) { + ctx, span := telemetry.NewSpan(ctx, "deployment-target-details") + defer span.End() + + var deploymentTarget DeploymentTarget + + if inp.ClusterID == 0 { + return deploymentTarget, telemetry.Error(ctx, span, nil, "cluster id is empty") + } + if inp.ProjectID == 0 { + return deploymentTarget, telemetry.Error(ctx, span, nil, "project id is empty") + } + if inp.DeploymentTargetID == "" { + return deploymentTarget, telemetry.Error(ctx, span, nil, "deployment target id is empty") + } + if inp.CCPClient == nil { + return deploymentTarget, telemetry.Error(ctx, span, nil, "cluster control plane client is nil") + } + + deploymentTargetDetailsReq := connect.NewRequest(&porterv1.DeploymentTargetDetailsRequest{ + ProjectId: inp.ProjectID, + DeploymentTargetId: inp.DeploymentTargetID, + }) + + deploymentTargetDetailsResp, err := inp.CCPClient.DeploymentTargetDetails(ctx, deploymentTargetDetailsReq) + if err != nil { + return deploymentTarget, telemetry.Error(ctx, span, err, "error getting deployment target details from cluster control plane client") + } + + if deploymentTargetDetailsResp == nil || deploymentTargetDetailsResp.Msg == nil { + return deploymentTarget, telemetry.Error(ctx, span, err, "deployment target details resp is nil") + } + + if deploymentTargetDetailsResp.Msg.ClusterId != inp.ClusterID { + return deploymentTarget, telemetry.Error(ctx, span, err, "deployment target details resp cluster id does not match cluster id") + } + + deploymentTarget = DeploymentTarget{ + Namespace: deploymentTargetDetailsResp.Msg.Namespace, + ClusterID: deploymentTargetDetailsResp.Msg.ClusterId, + } + + return deploymentTarget, nil +} diff --git a/internal/models/deployment_target.go b/internal/models/deployment_target.go index 0c5070b22c..67270769d4 100644 --- a/internal/models/deployment_target.go +++ b/internal/models/deployment_target.go @@ -31,4 +31,7 @@ type DeploymentTarget struct { // SelectorType is the kind of selector (i.e. NAMESPACE or LABEL). SelectorType DeploymentTargetSelectorType `json:"selector_type"` + + // Preview is a boolean indicating whether this target is a preview target. + Preview bool `gorm:"default:false" json:"preview"` } diff --git a/internal/porter_app/environment.go b/internal/porter_app/environment.go index 8cd7b06951..8d5cb8fbea 100644 --- a/internal/porter_app/environment.go +++ b/internal/porter_app/environment.go @@ -5,9 +5,9 @@ import ( "fmt" porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1" + "github.com/porter-dev/porter/internal/deployment_target" "github.com/porter-dev/porter/internal/kubernetes" "github.com/porter-dev/porter/internal/kubernetes/environment_groups" - "github.com/porter-dev/porter/internal/models" "github.com/porter-dev/porter/internal/repository" "github.com/porter-dev/porter/internal/telemetry" ) @@ -44,11 +44,11 @@ func WithoutDefaultAppEnvGroups() EnvVariableOption { // AppEnvironmentFromProtoInput is the input struct for AppEnvironmentFromProto type AppEnvironmentFromProtoInput struct { - ProjectID uint - ClusterID int - App *porterv1.PorterApp - K8SAgent *kubernetes.Agent - DeploymentTargetRepository repository.DeploymentTargetRepository + ProjectID uint + ClusterID int + DeploymentTarget deployment_target.DeploymentTarget + App *porterv1.PorterApp + K8SAgent *kubernetes.Agent } // AppEnvironmentFromProto returns all envfironment groups referenced in an app proto with their variables @@ -64,28 +64,11 @@ func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInp if inp.ClusterID == 0 { return nil, telemetry.Error(ctx, span, nil, "must provide a cluster id") } - if inp.K8SAgent == nil { - return nil, telemetry.Error(ctx, span, nil, "must provide a kubernetes agent") - } if inp.App == nil { return nil, telemetry.Error(ctx, span, nil, "must provide an app") } - - deploymentTargets, err := inp.DeploymentTargetRepository.List(inp.ProjectID) - if err != nil { - return envGroups, telemetry.Error(ctx, span, err, "error reading deployment targets") - } - - if len(deploymentTargets) == 0 { - return envGroups, telemetry.Error(ctx, span, nil, "no deployment targets found") - } - if len(deploymentTargets) > 1 { - return envGroups, telemetry.Error(ctx, span, nil, "more than one deployment target found") - } - - deploymentTarget := deploymentTargets[0] - if deploymentTarget.ClusterID != inp.ClusterID { - return envGroups, telemetry.Error(ctx, span, nil, "deployment target does not belong to cluster") + if inp.K8SAgent == nil { + return nil, telemetry.Error(ctx, span, nil, "must provide a kubernetes agent") } var opts envVariarableOptions @@ -93,14 +76,6 @@ func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInp opt(&opts) } - var namespace string - switch deploymentTarget.SelectorType { - case models.DeploymentTargetSelectorType_Namespace: - namespace = deploymentTarget.Selector - default: - return envGroups, telemetry.Error(ctx, span, nil, "deployment target selector type not supported") - } - filteredEnvGroups := inp.App.EnvGroups if len(opts.envGroups) > 0 { filteredEnvGroups = []*porterv1.EnvGroup{} @@ -117,7 +92,7 @@ func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInp envGroup, err := environment_groups.EnvironmentGroupInTargetNamespace(ctx, inp.K8SAgent, environment_groups.EnvironmentGroupInTargetNamespaceInput{ Name: envGroupRef.GetName(), Version: int(envGroupRef.GetVersion()), - Namespace: namespace, + Namespace: inp.DeploymentTarget.Namespace, ExcludeDefaultAppEnvironmentGroup: opts.excludeDefaultAppEnvGroups, }) if err != nil { diff --git a/internal/porter_app/revisions.go b/internal/porter_app/revisions.go index 29332acd44..a0e715ea94 100644 --- a/internal/porter_app/revisions.go +++ b/internal/porter_app/revisions.go @@ -10,6 +10,7 @@ import ( "github.com/porter-dev/api-contracts/generated/go/helpers" 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/deployment_target" "github.com/porter-dev/porter/internal/kubernetes" "github.com/porter-dev/porter/internal/kubernetes/environment_groups" "github.com/porter-dev/porter/internal/repository" @@ -105,12 +106,12 @@ func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevi b64 := base64.StdEncoding.EncodeToString(encoded) revision = Revision{ - B64AppProto: b64, - Status: appRevision.Status, - ID: appRevision.Id, - RevisionNumber: appRevision.RevisionNumber, - CreatedAt: appRevision.CreatedAt.AsTime(), - UpdatedAt: appRevision.UpdatedAt.AsTime(), + B64AppProto: b64, + Status: appRevision.Status, + ID: appRevision.Id, + RevisionNumber: appRevision.RevisionNumber, + CreatedAt: appRevision.CreatedAt.AsTime(), + UpdatedAt: appRevision.UpdatedAt.AsTime(), DeploymentTargetID: appRevision.DeploymentTargetId, } @@ -119,13 +120,12 @@ func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevi // AttachEnvToRevisionInput is the input struct for AttachEnvToRevision type AttachEnvToRevisionInput struct { - ProjectID uint - ClusterID int - DeploymentTargetID string - Revision Revision - K8SAgent *kubernetes.Agent - PorterAppRepository repository.PorterAppRepository - DeploymentTargetRepository repository.DeploymentTargetRepository + ProjectID uint + ClusterID int + Revision Revision + DeploymentTarget deployment_target.DeploymentTarget + K8SAgent *kubernetes.Agent + PorterAppRepository repository.PorterAppRepository } // AttachEnvToRevision attaches the environment variables from the app's default env group to a revision @@ -142,9 +142,6 @@ func AttachEnvToRevision(ctx context.Context, inp AttachEnvToRevisionInput) (Rev if inp.ClusterID == 0 { return revision, telemetry.Error(ctx, span, nil, "must provide a cluster id") } - if inp.DeploymentTargetID == "" { - return revision, telemetry.Error(ctx, span, nil, "must provide a deployment target id") - } if inp.K8SAgent == nil { return revision, telemetry.Error(ctx, span, nil, "k8s agent is nil") } @@ -160,18 +157,18 @@ func AttachEnvToRevision(ctx context.Context, inp AttachEnvToRevisionInput) (Rev return revision, telemetry.Error(ctx, span, err, "error unmarshalling app proto") } - envName, err := AppEnvGroupName(ctx, appDef.Name, inp.DeploymentTargetID, uint(inp.ClusterID), inp.PorterAppRepository) + envName, err := AppEnvGroupName(ctx, appDef.Name, inp.Revision.DeploymentTargetID, uint(inp.ClusterID), inp.PorterAppRepository) if err != nil { return revision, telemetry.Error(ctx, span, err, "error getting app env group name") } envNameFilter := []string{envName} envFromProtoInp := AppEnvironmentFromProtoInput{ - ProjectID: inp.ProjectID, - ClusterID: inp.ClusterID, - App: appDef, - K8SAgent: inp.K8SAgent, - DeploymentTargetRepository: inp.DeploymentTargetRepository, + ProjectID: inp.ProjectID, + ClusterID: inp.ClusterID, + App: appDef, + K8SAgent: inp.K8SAgent, + DeploymentTarget: inp.DeploymentTarget, } envGroups, err := AppEnvironmentFromProto(ctx, envFromProtoInp, WithEnvGroupFilter(envNameFilter), WithSecrets()) if err != nil { diff --git a/internal/repository/deployment_target.go b/internal/repository/deployment_target.go index 65ac52aa46..e32c07d238 100644 --- a/internal/repository/deployment_target.go +++ b/internal/repository/deployment_target.go @@ -10,4 +10,6 @@ type DeploymentTargetRepository interface { DeploymentTargetBySelectorAndSelectorType(projectID uint, clusterID uint, selector, selectorType string) (*models.DeploymentTarget, error) // List returns all deployment targets for a project List(projectID uint) ([]*models.DeploymentTarget, error) + // CreateDeploymentTarget creates a new deployment target + CreateDeploymentTarget(deploymentTarget *models.DeploymentTarget) (*models.DeploymentTarget, error) } diff --git a/internal/repository/gorm/deployment_target.go b/internal/repository/gorm/deployment_target.go index 18f87c286e..b57d47b386 100644 --- a/internal/repository/gorm/deployment_target.go +++ b/internal/repository/gorm/deployment_target.go @@ -2,6 +2,7 @@ package gorm import ( "errors" + "time" "github.com/google/uuid" "github.com/porter-dev/porter/internal/models" @@ -45,3 +46,38 @@ func (repo *DeploymentTargetRepository) List(projectID uint) ([]*models.Deployme return deploymentTargets, nil } + +// CreateDeploymentTarget creates a new deployment target +func (repo *DeploymentTargetRepository) CreateDeploymentTarget(deploymentTarget *models.DeploymentTarget) (*models.DeploymentTarget, error) { + if deploymentTarget == nil { + return nil, errors.New("deployment target is nil") + } + if deploymentTarget.Selector == "" { + return nil, errors.New("deployment target selector is empty") + } + if deploymentTarget.SelectorType == "" { + return nil, errors.New("deployment target selector type is empty") + } + if deploymentTarget.ClusterID == 0 { + return nil, errors.New("deployment target cluster id is empty") + } + if deploymentTarget.ProjectID == 0 { + return nil, errors.New("deployment target project id is empty") + } + + if deploymentTarget.ID == uuid.Nil { + deploymentTarget.ID = uuid.New() + } + if deploymentTarget.CreatedAt.IsZero() { + deploymentTarget.CreatedAt = time.Now().UTC() + } + if deploymentTarget.UpdatedAt.IsZero() { + deploymentTarget.UpdatedAt = time.Now().UTC() + } + + if err := repo.db.Create(deploymentTarget).Error; err != nil { + return nil, err + } + + return deploymentTarget, nil +} diff --git a/internal/repository/test/deployment_target.go b/internal/repository/test/deployment_target.go index 584f3c519c..084776fecd 100644 --- a/internal/repository/test/deployment_target.go +++ b/internal/repository/test/deployment_target.go @@ -26,3 +26,8 @@ func (repo *DeploymentTargetRepository) DeploymentTargetBySelectorAndSelectorTyp func (repo *DeploymentTargetRepository) List(projectID uint) ([]*models.DeploymentTarget, error) { return nil, errors.New("cannot read database") } + +// CreateDeploymentTarget creates a new deployment target +func (repo *DeploymentTargetRepository) CreateDeploymentTarget(deploymentTarget *models.DeploymentTarget) (*models.DeploymentTarget, error) { + return nil, errors.New("cannot write database") +}