ianedwards 2 лет назад
Родитель
Сommit
91e949ad8b

+ 5 - 9
api/client/porter_app.go

@@ -418,20 +418,16 @@ func (c *Client) UpdateRevisionStatus(
 func (c *Client) GetBuildEnv(
 func (c *Client) GetBuildEnv(
 	ctx context.Context,
 	ctx context.Context,
 	projectID uint, clusterID uint,
 	projectID uint, clusterID uint,
-	base64AppProto string,
+	appName string, appRevisionId string,
 ) (*porter_app.GetBuildEnvResponse, error) {
 ) (*porter_app.GetBuildEnvResponse, error) {
 	resp := &porter_app.GetBuildEnvResponse{}
 	resp := &porter_app.GetBuildEnvResponse{}
 
 
-	req := &porter_app.GetBuildEnvRequest{
-		Base64AppProto: base64AppProto,
-	}
-
-	err := c.postRequest(
+	err := c.getRequest(
 		fmt.Sprintf(
 		fmt.Sprintf(
-			"/projects/%d/clusters/%d/apps/build-env",
-			projectID, clusterID,
+			"/projects/%d/clusters/%d/apps/%s/revisions/%s/build-env",
+			projectID, clusterID, appName, appRevisionId,
 		),
 		),
-		req,
+		nil,
 		resp,
 		resp,
 	)
 	)
 
 

+ 168 - 0
api/server/handlers/porter_app/get_app_env.go

@@ -0,0 +1,168 @@
+package porter_app
+
+import (
+	"encoding/base64"
+	"net/http"
+
+	"github.com/google/uuid"
+	"github.com/porter-dev/api-contracts/generated/go/helpers"
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/api/server/authz"
+	"github.com/porter-dev/porter/api/server/handlers"
+	"github.com/porter-dev/porter/api/server/shared"
+	"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/kubernetes/environment_groups"
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/porter_app"
+	"github.com/porter-dev/porter/internal/telemetry"
+)
+
+// GetAppEnvHandler is the handler for the /apps/{porter_app_name}/revisions/{app_revision_id}/env endpoint
+type GetAppEnvHandler struct {
+	handlers.PorterHandlerReadWriter
+	authz.KubernetesAgentGetter
+}
+
+// NewGetAppEnvHandler handles GET requests to the /apps/{porter_app_name}/revisions/{app_revision_id}/env endpoint
+func NewGetAppEnvHandler(
+	config *config.Config,
+	decoderValidator shared.RequestDecoderValidator,
+	writer shared.ResultWriter,
+) *GetAppEnvHandler {
+	return &GetAppEnvHandler{
+		PorterHandlerReadWriter: handlers.NewDefaultPorterHandler(config, decoderValidator, writer),
+		KubernetesAgentGetter:   authz.NewOutOfClusterAgentGetter(config),
+	}
+}
+
+// GetAppEnvRequest is the request object for the /apps/{porter_app_name}/revisions/{app_revision_id}/env endpoint
+type GetAppEnvRequest struct {
+	// EnvGroups is a list of environment group names to query. If empty, all environment groups will be queried
+	EnvGroups []string `json:"env_groups"`
+}
+
+// GetAppEnvResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/env endpoint
+type GetAppEnvResponse struct {
+	EnvGroups []environment_groups.EnvironmentGroup `json:"env_groups"`
+}
+
+// ServeHTTP translates the request into a GetAppEnvRequest request, uses the revision proto to query the cluster for the requested env groups, and returns the response
+func (c *GetAppEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	ctx, span := telemetry.NewSpan(r.Context(), "serve-get-app-env")
+	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
+	}
+
+	revisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing app revision id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appRevisionUuid, err := uuid.Parse(revisionID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing app revision id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if appRevisionUuid == uuid.Nil {
+		err := telemetry.Error(ctx, span, nil, "app revision id is nil")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: appRevisionUuid.String()})
+
+	request := &GetAppEnvRequest{}
+	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
+	}
+
+	revision, err := porter_app.GetAppRevision(ctx, porter_app.GetAppRevisionInput{
+		AppRevisionID: appRevisionUuid,
+		ProjectID:     project.ID,
+		CCPClient:     c.Config().ClusterControlPlaneClient,
+	})
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting app revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	decoded, err := base64.StdEncoding.DecodeString(revision.B64AppProto)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error decoding base proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	appProto := &porterv1.PorterApp{}
+	err = helpers.UnmarshalContractObject(decoded, appProto)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error unmarshalling app proto")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+
+	deploymentTargets, err := c.Repo().DeploymentTarget().List(project.ID)
+	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 {
+		err := telemetry.Error(ctx, span, nil, "no deployment targets found")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+	if len(deploymentTargets) > 1 {
+		err = telemetry.Error(ctx, span, nil, "more than one deployment target found")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	deploymentTarget := deploymentTargets[0]
+	if deploymentTarget.ClusterID != int(cluster.ID) {
+		err := telemetry.Error(ctx, span, nil, "deployment target does not belong to cluster")
+		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 agent")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	envFromProtoInp := porter_app.AppEnvironmentFromProtoInput{
+		App:              appProto,
+		DeploymentTarget: deploymentTarget,
+		K8SAgent:         agent,
+	}
+	envGroups, err := porter_app.AppEnvironmentFromProto(ctx, envFromProtoInp, porter_app.WithEnvGroupFilter(request.EnvGroups), porter_app.WithSecrets())
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting app environment from revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
+
+	res := &GetAppEnvResponse{
+		EnvGroups: envGroups,
+	}
+
+	c.WriteResult(w, r, res)
+}

+ 30 - 15
api/server/handlers/porter_app/get_build_env.go

@@ -4,6 +4,7 @@ import (
 	"encoding/base64"
 	"encoding/base64"
 	"net/http"
 	"net/http"
 
 
+	"github.com/google/uuid"
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/api/server/authz"
 	"github.com/porter-dev/porter/api/server/authz"
@@ -11,19 +12,20 @@ import (
 	"github.com/porter-dev/porter/api/server/shared"
 	"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/apierrors"
 	"github.com/porter-dev/porter/api/server/shared/config"
 	"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/api/types"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/models"
 	"github.com/porter-dev/porter/internal/porter_app"
 	"github.com/porter-dev/porter/internal/porter_app"
 	"github.com/porter-dev/porter/internal/telemetry"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 )
 
 
-// GetBuildEnvHandler is the handler for the /apps/build-env endpoint
+// GetBuildEnvHandler is the handler for the /apps/{porter_app_name}/revisions/{app_revision_id}/build-env endpoint
 type GetBuildEnvHandler struct {
 type GetBuildEnvHandler struct {
 	handlers.PorterHandlerReadWriter
 	handlers.PorterHandlerReadWriter
 	authz.KubernetesAgentGetter
 	authz.KubernetesAgentGetter
 }
 }
 
 
-// NewGetBuildEnvHandler handles GET requests to the /apps/build-env endpoint
+// NewGetBuildEnvHandler handles GET requests to the /apps/{porter_app_name}/revisions/{app_revision_id}/build-env endpoint
 func NewGetBuildEnvHandler(
 func NewGetBuildEnvHandler(
 	config *config.Config,
 	config *config.Config,
 	decoderValidator shared.RequestDecoderValidator,
 	decoderValidator shared.RequestDecoderValidator,
@@ -35,12 +37,7 @@ func NewGetBuildEnvHandler(
 	}
 	}
 }
 }
 
 
-// GetBuildEnvRequest is the request object for the /apps/build-env endpoint
-type GetBuildEnvRequest struct {
-	Base64AppProto string `json:"b64_app_proto"`
-}
-
-// GetBuildEnvResponse is the response object for the /apps/build-env endpoint
+// GetBuildEnvResponse is the response object for the /apps/{porter_app_name}/revisions/{app_revision_id}/build-env endpoint
 type GetBuildEnvResponse struct {
 type GetBuildEnvResponse struct {
 	BuildEnvVariables map[string]string `json:"build_env_variables"`
 	BuildEnvVariables map[string]string `json:"build_env_variables"`
 }
 }
@@ -64,22 +61,40 @@ func (c *GetBuildEnvHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		return
 		return
 	}
 	}
 
 
-	request := &ApplyPorterAppRequest{}
-	if ok := c.DecodeAndValidate(w, r, request); !ok {
-		err := telemetry.Error(ctx, span, nil, "error decoding request")
+	revisionID, reqErr := requestutils.GetURLParamString(r, types.URLParamAppRevisionID)
+	if reqErr != nil {
+		err := telemetry.Error(ctx, span, nil, "error parsing app revision id")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 		return
 	}
 	}
 
 
-	if request.Base64AppProto == "" {
-		err := telemetry.Error(ctx, span, nil, "app proto is empty")
+	appRevisionUuid, err := uuid.Parse(revisionID)
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error parsing app revision id")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
+		return
+	}
+	if appRevisionUuid == uuid.Nil {
+		err := telemetry.Error(ctx, span, nil, "app revision id is nil")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 		return
 	}
 	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-revision-id", Value: appRevisionUuid.String()})
+
+	revision, err := porter_app.GetAppRevision(ctx, porter_app.GetAppRevisionInput{
+		AppRevisionID: appRevisionUuid,
+		ProjectID:     project.ID,
+		CCPClient:     c.Config().ClusterControlPlaneClient,
+	})
+	if err != nil {
+		err := telemetry.Error(ctx, span, err, "error getting app revision")
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+		return
+	}
 
 
-	decoded, err := base64.StdEncoding.DecodeString(request.Base64AppProto)
+	decoded, err := base64.StdEncoding.DecodeString(revision.B64AppProto)
 	if err != nil {
 	if err != nil {
-		err := telemetry.Error(ctx, span, err, "error decoding base yaml")
+		err := telemetry.Error(ctx, span, err, "error decoding base proto")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 		return
 	}
 	}

+ 33 - 4
api/server/router/porter_app.go

@@ -978,14 +978,14 @@ func getPorterAppRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
-	// POST /api/projects/{project_id}/clusters/{cluster_id}/apps/build-env -> porter_app.NewGetBuildEnvHandler
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id}/build-env -> porter_app.NewGetBuildEnvHandler
 	getBuildEnvEndpoint := factory.NewAPIEndpoint(
 	getBuildEnvEndpoint := factory.NewAPIEndpoint(
 		&types.APIRequestMetadata{
 		&types.APIRequestMetadata{
-			Verb:   types.APIVerbUpdate,
-			Method: types.HTTPVerbPost,
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
 			Path: &types.Path{
 			Path: &types.Path{
 				Parent:       basePath,
 				Parent:       basePath,
-				RelativePath: "/apps/build-env",
+				RelativePath: fmt.Sprintf("/apps/{%s}/revisions/{%s}/build-env", types.URLParamPorterAppName, types.URLParamAppRevisionID),
 			},
 			},
 			Scopes: []types.PermissionScope{
 			Scopes: []types.PermissionScope{
 				types.UserScope,
 				types.UserScope,
@@ -1036,5 +1036,34 @@ func getPorterAppRoutes(
 		Router:   r,
 		Router:   r,
 	})
 	})
 
 
+	// GET /api/projects/{project_id}/clusters/{cluster_id}/apps/{porter_app_name}/revisions/{app_revision_id}/env -> porter_app.NewGetAppEnvHandler
+	getAppEnvEndpoint := factory.NewAPIEndpoint(
+		&types.APIRequestMetadata{
+			Verb:   types.APIVerbGet,
+			Method: types.HTTPVerbGet,
+			Path: &types.Path{
+				Parent:       basePath,
+				RelativePath: fmt.Sprintf("/apps/{%s}/revisions/{%s}/env", types.URLParamPorterAppName, types.URLParamAppRevisionID),
+			},
+			Scopes: []types.PermissionScope{
+				types.UserScope,
+				types.ProjectScope,
+				types.ClusterScope,
+			},
+		},
+	)
+
+	getAppEnvHandler := porter_app.NewGetAppEnvHandler(
+		config,
+		factory.GetDecoderValidator(),
+		factory.GetResultWriter(),
+	)
+
+	routes = append(routes, &router.Route{
+		Endpoint: getAppEnvEndpoint,
+		Handler:  getAppEnvHandler,
+		Router:   r,
+	})
+
 	return routes, newPath
 	return routes, newPath
 }
 }

+ 1 - 1
cli/cmd/v2/apply.go

@@ -150,7 +150,7 @@ func Apply(ctx context.Context, cliConf config.CLIConfig, client api.Client, por
 		buildSettings.CurrentImageTag = currentImageTag
 		buildSettings.CurrentImageTag = currentImageTag
 		buildSettings.ProjectID = cliConf.Project
 		buildSettings.ProjectID = cliConf.Project
 
 
-		buildEnv, err := client.GetBuildEnv(ctx, cliConf.Project, cliConf.Cluster, base64AppProto)
+		buildEnv, err := client.GetBuildEnv(ctx, cliConf.Project, cliConf.Cluster, appName, targetResp.DeploymentTargetID)
 		if err != nil {
 		if err != nil {
 			return fmt.Errorf("error getting build env: %w", err)
 			return fmt.Errorf("error getting build env: %w", err)
 		}
 		}

+ 1 - 1
go.mod

@@ -81,7 +81,7 @@ require (
 	github.com/matryer/is v1.4.0
 	github.com/matryer/is v1.4.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/nats-io/nats.go v1.24.0
 	github.com/open-policy-agent/opa v0.44.0
 	github.com/open-policy-agent/opa v0.44.0
-	github.com/porter-dev/api-contracts v0.1.4
+	github.com/porter-dev/api-contracts v0.1.5
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/riandyrn/otelchi v0.5.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/santhosh-tekuri/jsonschema/v5 v5.0.1
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d
 	github.com/stefanmcshane/helm v0.0.0-20221213002717-88a4a2c6e77d

+ 2 - 2
go.sum

@@ -1512,8 +1512,8 @@ github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.1.4 h1:EOEd+vgR/Jjt4K8NPKQsxpZ8wq/7jKbnN2sabQnuSqY=
-github.com/porter-dev/api-contracts v0.1.4/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.1.5 h1:Nz0bEIXedKHYfC0YzOj579ABXHxDaH4DXjKcstvWR8A=
+github.com/porter-dev/api-contracts v0.1.5/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3 h1:dBuYkiVLa5Ce7059d6qTe9a1C2XEORFEanhbtV92R+M=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/porter-dev/switchboard v0.0.3/go.mod h1:xSPzqSFMQ6OSbp42fhCi4AbGbQbsm6nRvOkrblFeXU4=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 1 - 1
internal/kubernetes/environment_groups/list.go

@@ -31,7 +31,7 @@ type EnvironmentGroup struct {
 	// Variables are non-secret values for the EnvironmentGroup. This usually will be a configmap
 	// Variables are non-secret values for the EnvironmentGroup. This usually will be a configmap
 	Variables map[string]string `json:"variables"`
 	Variables map[string]string `json:"variables"`
 	// SecretVariables are secret values for the EnvironmentGroup. This usually will be a Secret on the kubernetes cluster
 	// SecretVariables are secret values for the EnvironmentGroup. This usually will be a Secret on the kubernetes cluster
-	SecretVariables map[string][]byte `json:"variables_secrets"`
+	SecretVariables map[string][]byte `json:"variables_secrets,omitempty"`
 	// CreatedAt is only used for display purposes and is in UTC Unix time
 	// CreatedAt is only used for display purposes and is in UTC Unix time
 	CreatedAtUTC time.Time `json:"created_at"`
 	CreatedAtUTC time.Time `json:"created_at"`
 }
 }

+ 23 - 3
internal/porter_app/environment.go

@@ -14,6 +14,7 @@ import (
 
 
 type envVariarableOptions struct {
 type envVariarableOptions struct {
 	includeSecrets bool
 	includeSecrets bool
+	envGroups      []string
 }
 }
 
 
 // EnvVariableOption is a function that modifies AppEnvironmentFromProto
 // EnvVariableOption is a function that modifies AppEnvironmentFromProto
@@ -26,6 +27,13 @@ func WithSecrets() EnvVariableOption {
 	}
 	}
 }
 }
 
 
+// WithEnvGroupFilter filters the environment groups to only include the ones in this list of names
+func WithEnvGroupFilter(envGroups []string) EnvVariableOption {
+	return func(opts *envVariarableOptions) {
+		opts.envGroups = envGroups
+	}
+}
+
 // AppEnvironmentFromProtoInput is the input struct for AppEnvironmentFromProto
 // AppEnvironmentFromProtoInput is the input struct for AppEnvironmentFromProto
 type AppEnvironmentFromProtoInput struct {
 type AppEnvironmentFromProtoInput struct {
 	App              *porterv1.PorterApp
 	App              *porterv1.PorterApp
@@ -38,7 +46,7 @@ func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInp
 	ctx, span := telemetry.NewSpan(ctx, "porter-app-env-from-proto")
 	ctx, span := telemetry.NewSpan(ctx, "porter-app-env-from-proto")
 	defer span.End()
 	defer span.End()
 
 
-	var envGroups []environment_groups.EnvironmentGroup
+	envGroups := []environment_groups.EnvironmentGroup{}
 
 
 	if inp.DeploymentTarget == nil {
 	if inp.DeploymentTarget == nil {
 		return nil, telemetry.Error(ctx, span, nil, "must provide a deployment target")
 		return nil, telemetry.Error(ctx, span, nil, "must provide a deployment target")
@@ -63,14 +71,26 @@ func AppEnvironmentFromProto(ctx context.Context, inp AppEnvironmentFromProtoInp
 		return envGroups, telemetry.Error(ctx, span, nil, "deployment target selector type not supported")
 		return envGroups, telemetry.Error(ctx, span, nil, "deployment target selector type not supported")
 	}
 	}
 
 
-	for _, envGroupRef := range inp.App.EnvGroups {
+	filteredEnvGroups := inp.App.EnvGroups
+	if len(opts.envGroups) > 0 {
+		filteredEnvGroups = []*porterv1.EnvGroup{}
+		for _, envGroup := range inp.App.EnvGroups {
+			for _, envGroupName := range opts.envGroups {
+				if envGroup.GetName() == envGroupName {
+					filteredEnvGroups = append(filteredEnvGroups, envGroup)
+				}
+			}
+		}
+	}
+
+	for _, envGroupRef := range filteredEnvGroups {
 		envGroup, err := environment_groups.EnvironmentGroupInTargetNamespace(ctx, inp.K8SAgent, environment_groups.EnvironmentGroupInTargetNamespaceInput{
 		envGroup, err := environment_groups.EnvironmentGroupInTargetNamespace(ctx, inp.K8SAgent, environment_groups.EnvironmentGroupInTargetNamespaceInput{
 			Name:      envGroupRef.GetName(),
 			Name:      envGroupRef.GetName(),
 			Version:   int(envGroupRef.GetVersion()),
 			Version:   int(envGroupRef.GetVersion()),
 			Namespace: namespace,
 			Namespace: namespace,
 		})
 		})
 		if err != nil {
 		if err != nil {
-			return nil, err
+			return nil, telemetry.Error(ctx, span, err, "error getting environment group in target namespace")
 		}
 		}
 
 
 		if !opts.includeSecrets {
 		if !opts.includeSecrets {

+ 48 - 0
internal/porter_app/revisions.go

@@ -5,8 +5,11 @@ import (
 	"encoding/base64"
 	"encoding/base64"
 	"time"
 	"time"
 
 
+	"connectrpc.com/connect"
+	"github.com/google/uuid"
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	"github.com/porter-dev/api-contracts/generated/go/helpers"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	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"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 )
 
 
@@ -26,6 +29,51 @@ type Revision struct {
 	UpdatedAt time.Time `json:"updated_at"`
 	UpdatedAt time.Time `json:"updated_at"`
 }
 }
 
 
+// GetAppRevisionInput is the input struct for GetAppRevisions
+type GetAppRevisionInput struct {
+	ProjectID     uint
+	AppRevisionID uuid.UUID
+
+	CCPClient porterv1connect.ClusterControlPlaneServiceClient
+}
+
+// GetAppRevision returns a single app revision by id
+func GetAppRevision(ctx context.Context, inp GetAppRevisionInput) (Revision, error) {
+	ctx, span := telemetry.NewSpan(ctx, "get-app-revision")
+	defer span.End()
+
+	var revision Revision
+
+	if inp.ProjectID == 0 {
+		return revision, telemetry.Error(ctx, span, nil, "must provide a project id")
+	}
+	if inp.AppRevisionID == uuid.Nil {
+		return revision, telemetry.Error(ctx, span, nil, "must provide an app revision id")
+	}
+
+	getRevisionReq := connect.NewRequest(&porterv1.GetAppRevisionRequest{
+		ProjectId:     int64(inp.ProjectID),
+		AppRevisionId: inp.AppRevisionID.String(),
+	})
+
+	ccpResp, err := inp.CCPClient.GetAppRevision(ctx, getRevisionReq)
+	if err != nil {
+		return revision, telemetry.Error(ctx, span, err, "error getting app revision")
+	}
+	if ccpResp == nil || ccpResp.Msg == nil {
+		return revision, telemetry.Error(ctx, span, nil, "get app revision response is nil")
+	}
+
+	appRevisionProto := ccpResp.Msg.AppRevision
+
+	revision, err = EncodedRevisionFromProto(ctx, appRevisionProto)
+	if err != nil {
+		return revision, telemetry.Error(ctx, span, err, "error converting app revision from proto")
+	}
+
+	return revision, nil
+}
+
 // EncodedRevisionFromProto converts an AppRevision proto object into a Revision object
 // EncodedRevisionFromProto converts an AppRevision proto object into a Revision object
 func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevision) (Revision, error) {
 func EncodedRevisionFromProto(ctx context.Context, appRevision *porterv1.AppRevision) (Revision, error) {
 	ctx, span := telemetry.NewSpan(ctx, "encoded-revision-from-proto")
 	ctx, span := telemetry.NewSpan(ctx, "encoded-revision-from-proto")