Ver Fonte

POR-1816 parse out preview overrides from porter yaml (#3662)

ianedwards há 2 anos atrás
pai
commit
9582b94017

+ 46 - 11
api/server/handlers/porter_app/parse_yaml.go

@@ -1,10 +1,12 @@
 package porter_app
 
 import (
+	"context"
 	"encoding/base64"
 	"net/http"
 
 	"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/internal/porter_app"
 
@@ -40,13 +42,20 @@ type ParsePorterYAMLToProtoRequest struct {
 	AppName string `json:"app_name"`
 }
 
-// ParsePorterYAMLToProtoResponse is the response object for the /apps/parse endpoint
-type ParsePorterYAMLToProtoResponse struct {
+// EncodedAppWithEnv is a struct that contains a base64-encoded app proto object and a map of env variables
+type EncodedAppWithEnv struct {
 	B64AppProto  string            `json:"b64_app_proto"`
 	EnvVariables map[string]string `json:"env_variables"`
 	EnvSecrets   map[string]string `json:"env_secrets"`
 }
 
+// ParsePorterYAMLToProtoResponse is the response object for the /apps/parse endpoint
+type ParsePorterYAMLToProtoResponse struct {
+	EncodedAppWithEnv
+	// PreviewApp contains preview environment specific overrides, if they exist
+	PreviewApp *EncodedAppWithEnv `json:"preview_app,omitempty"`
+}
+
 // ServeHTTP receives a base64-encoded porter.yaml, parses the version, and then translates it into a base64-encoded app proto object
 func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	ctx, span := telemetry.NewSpan(r.Context(), "serve-parse-porter-yaml")
@@ -85,31 +94,57 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	appProto, envVariables, err := porter_app.ParseYAML(ctx, yaml, request.AppName)
+	appDefinition, err := porter_app.ParseYAML(ctx, yaml, request.AppName)
 	if err != nil {
 		err := telemetry.Error(ctx, span, err, "error parsing yaml")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 		return
 	}
-	if appProto == nil {
+	if appDefinition.AppProto == nil {
 		err := telemetry.Error(ctx, span, nil, "app proto is nil")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
 
-	by, err := helpers.MarshalContractObject(ctx, appProto)
+	response := &ParsePorterYAMLToProtoResponse{}
+
+	encodedApp, err := encodeAppProto(ctx, appDefinition.AppProto)
 	if err != nil {
-		err := telemetry.Error(ctx, span, nil, "error marshalling app proto")
+		err := telemetry.Error(ctx, span, err, "error encoding app proto")
 		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
 		return
 	}
+	response.B64AppProto = encodedApp
+	response.EnvVariables = appDefinition.EnvVariables
+
+	if appDefinition.PreviewApp != nil {
+		encodedPreviewApp, err := encodeAppProto(ctx, appDefinition.PreviewApp.AppProto)
+		if err != nil {
+			err := telemetry.Error(ctx, span, err, "error encoding preview app proto")
+			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusInternalServerError))
+			return
+		}
+		response.PreviewApp = &EncodedAppWithEnv{
+			B64AppProto:  encodedPreviewApp,
+			EnvVariables: appDefinition.PreviewApp.EnvVariables,
+		}
+	}
 
-	b64 := base64.StdEncoding.EncodeToString(by)
+	c.WriteResult(w, r, response)
+}
+
+func encodeAppProto(ctx context.Context, app *porterv1.PorterApp) (string, error) {
+	ctx, span := telemetry.NewSpan(ctx, "encode-app-proto")
+	defer span.End()
 
-	response := &ParsePorterYAMLToProtoResponse{
-		B64AppProto:  b64,
-		EnvVariables: envVariables,
+	var encodedApp string
+
+	by, err := helpers.MarshalContractObject(ctx, app)
+	if err != nil {
+		return encodedApp, err
 	}
 
-	c.WriteResult(w, r, response)
+	encodedApp = base64.StdEncoding.EncodeToString(by)
+
+	return encodedApp, nil
 }

+ 20 - 19
internal/porter_app/parse.go

@@ -8,7 +8,6 @@ import (
 
 	"sigs.k8s.io/yaml"
 
-	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/internal/telemetry"
 )
 
@@ -23,56 +22,58 @@ const (
 )
 
 // ParseYAML converts a Porter YAML file into a PorterApp proto object
-func ParseYAML(ctx context.Context, porterYaml []byte, appName string) (*porterv1.PorterApp, map[string]string, error) {
+func ParseYAML(ctx context.Context, porterYaml []byte, appName string) (v2.AppWithPreviewOverrides, error) {
 	ctx, span := telemetry.NewSpan(ctx, "porter-app-parse-yaml")
 	defer span.End()
 
+	var appDefinition v2.AppWithPreviewOverrides
+
 	if porterYaml == nil {
-		return nil, nil, telemetry.Error(ctx, span, nil, "porter yaml input is nil")
+		return appDefinition, telemetry.Error(ctx, span, nil, "porter yaml input is nil")
 	}
 
 	version := &yamlVersion{}
 	err := yaml.Unmarshal(porterYaml, version)
 	if err != nil {
-		return nil, nil, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml")
+		return appDefinition, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml")
 	}
 
-	var appProto *porterv1.PorterApp
-	var envVariables map[string]string
-
 	switch version.Version {
 	case PorterYamlVersion_V2:
-		appProto, envVariables, err = v2.AppProtoFromYaml(ctx, porterYaml)
+		appDefinition, err = v2.AppProtoFromYaml(ctx, porterYaml)
 		if err != nil {
-			return nil, nil, telemetry.Error(ctx, span, err, "error converting v2 yaml to proto")
+			return appDefinition, telemetry.Error(ctx, span, err, "error converting v2 yaml to proto")
 		}
 	// backwards compatibility for old porter.yaml files
 	// track this span in telemetry and reach out to customers who are still using old porter.yaml if they exist.
 	// once no one is converting from old porter.yaml, we can remove this code
 	case PorterYamlVersion_V1, "":
-		appProto, envVariables, err = v1.AppProtoFromYaml(ctx, porterYaml)
+		appProto, envVariables, err := v1.AppProtoFromYaml(ctx, porterYaml)
 		if err != nil {
-			return nil, nil, telemetry.Error(ctx, span, err, "error converting v1 yaml to proto")
+			return appDefinition, telemetry.Error(ctx, span, err, "error converting v1 yaml to proto")
 		}
+
+		appDefinition.AppProto = appProto
+		appDefinition.EnvVariables = envVariables
 	default:
-		return nil, nil, telemetry.Error(ctx, span, nil, "porter yaml version not supported")
+		return appDefinition, telemetry.Error(ctx, span, nil, "porter yaml version not supported")
 	}
 
-	if appProto == nil {
-		return nil, nil, telemetry.Error(ctx, span, nil, "porter yaml output is nil")
+	if appDefinition.AppProto == nil {
+		return appDefinition, telemetry.Error(ctx, span, nil, "porter yaml output is nil")
 	}
 
 	if appName != "" {
 		telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "override-name", Value: appName})
-		if appProto.Name != "" && appProto.Name != appName {
-			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "parsed-name", Value: appProto.Name})
-			return nil, nil, telemetry.Error(ctx, span, nil, "name specified in porter.yaml does not match app name")
+		if appDefinition.AppProto.Name != "" && appDefinition.AppProto.Name != appName {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "parsed-name", Value: appDefinition.AppProto.Name})
+			return appDefinition, telemetry.Error(ctx, span, nil, "name specified in porter.yaml does not match app name")
 		}
 
-		appProto.Name = appName
+		appDefinition.AppProto.Name = appName
 	}
 
-	return appProto, envVariables, nil
+	return appDefinition, nil
 }
 
 // yamlVersion is a struct used to unmarshal the version field of a Porter YAML file

+ 3 - 3
internal/porter_app/parse_test.go

@@ -31,12 +31,12 @@ func TestParseYAML(t *testing.T) {
 			want, err := os.ReadFile(fmt.Sprintf("testdata/%s.yaml", tt.porterYamlFileName))
 			is.NoErr(err) // no error expected reading test file
 
-			got, env, err := ParseYAML(context.Background(), want, "test-app")
+			got, err := ParseYAML(context.Background(), want, "test-app")
 			is.NoErr(err) // umbrella chart values should convert to map[string]any without issues
 
-			diffProtoWithFailTest(t, is, tt.want, got)
+			diffProtoWithFailTest(t, is, tt.want, got.AppProto)
 
-			is.Equal(env, map[string]string{
+			is.Equal(got.EnvVariables, map[string]string{
 				"PORT":     "8080",
 				"NODE_ENV": "production",
 			})

+ 104 - 59
internal/porter_app/v2/yaml.go

@@ -6,90 +6,63 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/ghodss/yaml"
 	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
 	"github.com/porter-dev/porter/internal/telemetry"
+	"gopkg.in/yaml.v2"
 )
 
+// AppProtoWithEnv is a struct containing a PorterApp proto object and its environment variables
+type AppProtoWithEnv struct {
+	AppProto     *porterv1.PorterApp
+	EnvVariables map[string]string
+}
+
+// AppWithPreviewOverrides is a porter app definition with its preview app definition, if it exists
+type AppWithPreviewOverrides struct {
+	AppProtoWithEnv
+	PreviewApp *AppProtoWithEnv
+}
+
 // AppProtoFromYaml converts a Porter YAML file into a PorterApp proto object
-func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte) (*porterv1.PorterApp, map[string]string, error) {
+func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte) (AppWithPreviewOverrides, error) {
 	ctx, span := telemetry.NewSpan(ctx, "v2-app-proto-from-yaml")
 	defer span.End()
 
+	var out AppWithPreviewOverrides
+
 	if porterYamlBytes == nil {
-		return nil, nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
+		return out, telemetry.Error(ctx, span, nil, "porter yaml is nil")
 	}
 
 	porterYaml := &PorterYAML{}
 	err := yaml.Unmarshal(porterYamlBytes, porterYaml)
 	if err != nil {
-		return nil, nil, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml")
-	}
-
-	appProto := &porterv1.PorterApp{
-		Name: porterYaml.Name,
-	}
-
-	if porterYaml.Build != nil {
-		appProto.Build = &porterv1.Build{
-			Context:    porterYaml.Build.Context,
-			Method:     porterYaml.Build.Method,
-			Builder:    porterYaml.Build.Builder,
-			Buildpacks: porterYaml.Build.Buildpacks,
-			Dockerfile: porterYaml.Build.Dockerfile,
-		}
-	}
-
-	if porterYaml.Image != nil {
-		appProto.Image = &porterv1.AppImage{
-			Repository: porterYaml.Image.Repository,
-			Tag:        porterYaml.Image.Tag,
-		}
+		return out, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml")
 	}
 
-	if porterYaml.Services == nil {
-		return nil, nil, telemetry.Error(ctx, span, nil, "porter yaml is missing services")
-	}
-
-	services := make(map[string]*porterv1.Service, 0)
-	for name, service := range porterYaml.Services {
-		serviceType, err := protoEnumFromType(name, service)
-		if err != nil {
-			return nil, nil, telemetry.Error(ctx, span, err, "error getting service type")
-		}
-
-		serviceProto, err := serviceProtoFromConfig(service, serviceType)
-		if err != nil {
-			return nil, nil, telemetry.Error(ctx, span, err, "error casting service config")
-		}
-
-		services[name] = serviceProto
+	appProto, envVariables, err := buildAppProto(ctx, porterYaml.PorterApp)
+	if err != nil {
+		return out, telemetry.Error(ctx, span, err, "error converting porter yaml to proto")
 	}
-	appProto.Services = services
+	out.AppProto = appProto
+	out.EnvVariables = envVariables
 
-	if porterYaml.Predeploy != nil {
-		predeployProto, err := serviceProtoFromConfig(*porterYaml.Predeploy, porterv1.ServiceType_SERVICE_TYPE_JOB)
+	if porterYaml.Previews != nil {
+		previewAppProto, previewEnvVariables, err := buildAppProto(ctx, *porterYaml.Previews)
 		if err != nil {
-			return nil, nil, telemetry.Error(ctx, span, err, "error casting predeploy config")
+			return out, telemetry.Error(ctx, span, err, "error converting preview porter yaml to proto")
 		}
-		appProto.Predeploy = predeployProto
-	}
-
-	envGroups := make([]*porterv1.EnvGroup, 0)
-	if porterYaml.EnvGroups != nil {
-		for _, envGroupName := range porterYaml.EnvGroups {
-			envGroups = append(envGroups, &porterv1.EnvGroup{
-				Name: envGroupName,
-			})
+		out.PreviewApp = &AppProtoWithEnv{
+			AppProto:     previewAppProto,
+			EnvVariables: previewEnvVariables,
 		}
 	}
-	appProto.EnvGroups = envGroups
 
-	return appProto, porterYaml.Env, nil
+	return out, nil
 }
 
-// PorterYAML represents all the possible fields in a Porter YAML file
-type PorterYAML struct {
+// PorterApp represents all the possible fields in a Porter YAML file
+type PorterApp struct {
 	Name     string             `yaml:"name"`
 	Services map[string]Service `yaml:"services"`
 	Image    *Image             `yaml:"image"`
@@ -100,6 +73,12 @@ type PorterYAML struct {
 	EnvGroups []string `yaml:"envGroups,omitempty"`
 }
 
+// PorterYAML represents all the possible fields in a Porter YAML file
+type PorterYAML struct {
+	PorterApp `yaml:",inline"`
+	Previews  *PorterApp `yaml:"previews,omitempty"`
+}
+
 // Build represents the build settings for a Porter app
 type Build struct {
 	Context    string   `yaml:"context" validate:"dir"`
@@ -151,6 +130,72 @@ type Image struct {
 	Tag        string `yaml:"tag"`
 }
 
+func buildAppProto(ctx context.Context, porterApp PorterApp) (*porterv1.PorterApp, map[string]string, error) {
+	ctx, span := telemetry.NewSpan(ctx, "build-app-proto")
+	defer span.End()
+
+	appProto := &porterv1.PorterApp{
+		Name: porterApp.Name,
+	}
+
+	if porterApp.Build != nil {
+		appProto.Build = &porterv1.Build{
+			Context:    porterApp.Build.Context,
+			Method:     porterApp.Build.Method,
+			Builder:    porterApp.Build.Builder,
+			Buildpacks: porterApp.Build.Buildpacks,
+			Dockerfile: porterApp.Build.Dockerfile,
+		}
+	}
+
+	if porterApp.Image != nil {
+		appProto.Image = &porterv1.AppImage{
+			Repository: porterApp.Image.Repository,
+			Tag:        porterApp.Image.Tag,
+		}
+	}
+
+	if porterApp.Services == nil {
+		return appProto, nil, telemetry.Error(ctx, span, nil, "porter yaml is missing services")
+	}
+
+	services := make(map[string]*porterv1.Service, 0)
+	for name, service := range porterApp.Services {
+		serviceType, err := protoEnumFromType(name, service)
+		if err != nil {
+			return appProto, nil, telemetry.Error(ctx, span, err, "error getting service type")
+		}
+
+		serviceProto, err := serviceProtoFromConfig(service, serviceType)
+		if err != nil {
+			return appProto, nil, telemetry.Error(ctx, span, err, "error casting service config")
+		}
+
+		services[name] = serviceProto
+	}
+	appProto.Services = services
+
+	if porterApp.Predeploy != nil {
+		predeployProto, err := serviceProtoFromConfig(*porterApp.Predeploy, porterv1.ServiceType_SERVICE_TYPE_JOB)
+		if err != nil {
+			return appProto, nil, telemetry.Error(ctx, span, err, "error casting predeploy config")
+		}
+		appProto.Predeploy = predeployProto
+	}
+
+	envGroups := make([]*porterv1.EnvGroup, 0)
+	if porterApp.EnvGroups != nil {
+		for _, envGroupName := range porterApp.EnvGroups {
+			envGroups = append(envGroups, &porterv1.EnvGroup{
+				Name: envGroupName,
+			})
+		}
+	}
+	appProto.EnvGroups = envGroups
+
+	return appProto, porterApp.Env, nil
+}
+
 func protoEnumFromType(name string, service Service) (porterv1.ServiceType, error) {
 	var serviceType porterv1.ServiceType