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

parse env var references from porter.yaml (#4134)

ianedwards 2 лет назад
Родитель
Сommit
70bbecd40b

+ 45 - 7
api/server/handlers/porter_app/yaml_from_revision.go

@@ -194,11 +194,11 @@ type formatDefaultEnvGroupInput struct {
 	PorterAppRepository       repository.PorterAppRepository
 }
 
-func defaultEnvGroup(ctx context.Context, input formatDefaultEnvGroupInput) (map[string]string, string, error) {
+func defaultEnvGroup(ctx context.Context, input formatDefaultEnvGroupInput) ([]v2.EnvVariableDefinition, string, error) {
 	ctx, span := telemetry.NewSpan(ctx, "format-default-env-group")
 	defer span.End()
 
-	env := map[string]string{}
+	var env []v2.EnvVariableDefinition
 
 	revision, err := porter_app.GetAppRevision(ctx, porter_app.GetAppRevisionInput{
 		AppRevisionID: input.AppRevisionID,
@@ -243,10 +243,46 @@ func defaultEnvGroup(ctx context.Context, input formatDefaultEnvGroupInput) (map
 	}
 
 	for key, val := range revisionWithEnv.Env.Variables {
-		env[key] = val
+		env = append(env, v2.EnvVariableDefinition{
+			Key:    key,
+			Source: v2.EnvVariableSource_Value,
+			Value: v2.EnvValueOptional{
+				Value: val,
+				IsSet: true,
+			},
+		})
 	}
 	for key, val := range revisionWithEnv.Env.SecretVariables {
-		env[key] = val
+		env = append(env, v2.EnvVariableDefinition{
+			Key:    key,
+			Source: v2.EnvVariableSource_Value,
+			Value: v2.EnvValueOptional{
+				Value: val,
+				IsSet: true,
+			},
+		})
+	}
+
+	for _, ev := range appProto.Env {
+		if ev.Source == porterv1.EnvVariableSource_ENV_VARIABLE_SOURCE_FROM_APP {
+			fromAppProto := ev.GetFromApp()
+			if fromAppProto == nil {
+				continue
+			}
+
+			fromApp, err := v2.EnvVarFromAppFromProto(fromAppProto)
+			if err != nil {
+				return env, "", telemetry.Error(ctx, span, err, "error converting env var from app to proto")
+			}
+
+			envVar := v2.EnvVariableDefinition{
+				Key:     ev.Key,
+				Source:  v2.EnvVariableSource_FromApp,
+				FromApp: fromApp,
+			}
+
+			env = append(env, envVar)
+		}
 	}
 
 	return env, revisionWithEnv.Env.Name, nil
@@ -280,11 +316,13 @@ func formatForExport(app v2.PorterApp, appRootDomain string) v2.PorterApp {
 	}
 
 	// remove env secrets from env
-	for key, val := range app.Env {
-		if val == "********" {
-			delete(app.Env, key)
+	var filtered []v2.EnvVariableDefinition
+	for _, ev := range app.Env {
+		if ev.Value.Value != "********" {
+			filtered = append(filtered, ev)
 		}
 	}
+	app.Env = filtered
 
 	// don't show env group versions
 	for i := range app.EnvGroups {

+ 7 - 7
dashboard/package-lock.json

@@ -95,7 +95,7 @@
         "@babel/preset-typescript": "^7.15.0",
         "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
         "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-        "@porter-dev/api-contracts": "^0.2.81",
+        "@porter-dev/api-contracts": "^0.2.84",
         "@testing-library/jest-dom": "^4.2.4",
         "@testing-library/react": "^9.3.2",
         "@testing-library/user-event": "^7.1.2",
@@ -2754,9 +2754,9 @@
       }
     },
     "node_modules/@porter-dev/api-contracts": {
-      "version": "0.2.81",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.81.tgz",
-      "integrity": "sha512-YrB0P8gbo1z2Eh5iYkU+BWPI6k8mSi22yyOl5K5xkV8L2X22APIdtUSy6gkEck7qrDvuZoxLs74GFsL7gNbvAg==",
+      "version": "0.2.84",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.84.tgz",
+      "integrity": "sha512-KNwaVBLkW95LQSxkxs+mwtJp8MIArS0X9ZODpVPjW/ZPjQ+PvCWoOsyNShoyOp2YrzaByKcXBDeKCsT2ZCgdCw==",
       "dev": true,
       "dependencies": {
         "@bufbuild/protobuf": "^1.1.0"
@@ -20056,9 +20056,9 @@
       "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
     },
     "@porter-dev/api-contracts": {
-      "version": "0.2.81",
-      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.81.tgz",
-      "integrity": "sha512-YrB0P8gbo1z2Eh5iYkU+BWPI6k8mSi22yyOl5K5xkV8L2X22APIdtUSy6gkEck7qrDvuZoxLs74GFsL7gNbvAg==",
+      "version": "0.2.84",
+      "resolved": "https://registry.npmjs.org/@porter-dev/api-contracts/-/api-contracts-0.2.84.tgz",
+      "integrity": "sha512-KNwaVBLkW95LQSxkxs+mwtJp8MIArS0X9ZODpVPjW/ZPjQ+PvCWoOsyNShoyOp2YrzaByKcXBDeKCsT2ZCgdCw==",
       "dev": true,
       "requires": {
         "@bufbuild/protobuf": "^1.1.0"

+ 1 - 1
dashboard/package.json

@@ -102,7 +102,7 @@
     "@babel/preset-typescript": "^7.15.0",
     "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
     "@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
-    "@porter-dev/api-contracts": "^0.2.81",
+    "@porter-dev/api-contracts": "^0.2.84",
     "@testing-library/jest-dom": "^4.2.4",
     "@testing-library/react": "^9.3.2",
     "@testing-library/user-event": "^7.1.2",

+ 1 - 1
go.mod

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

+ 2 - 2
go.sum

@@ -1520,8 +1520,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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/polyfloyd/go-errorlint v0.0.0-20210722154253-910bb7978349/go.mod h1:wi9BfjxjF/bwiZ701TzmfKu6UKC357IOAtNr0Td0Lvw=
-github.com/porter-dev/api-contracts v0.2.83 h1:UndRYlFMUbDa6oZaMpE8Q8sUpEfq+MOOf4pP/7MX1gw=
-github.com/porter-dev/api-contracts v0.2.83/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
+github.com/porter-dev/api-contracts v0.2.84 h1:HaqhaNgCR8ll86ExczPr6+eTIIYfNUujwVV2GXha9rQ=
+github.com/porter-dev/api-contracts v0.2.84/go.mod h1:fX6JmP5QuzxDLvqP3evFOTXjI4dHxsG0+VKNTjImZU8=
 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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=

+ 231 - 0
internal/porter_app/v2/env.go

@@ -0,0 +1,231 @@
+package v2
+
+import (
+	"fmt"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+)
+
+// Env is a list of env variable definitions
+type Env []EnvVariableDefinition
+
+// EnvVariableSource indicates how to set an env variable
+type EnvVariableSource string
+
+const (
+	// EnvVariableSource_Value indicates that the env variable should be set to an explicitly provided value
+	EnvVariableSource_Value EnvVariableSource = "value"
+	// EnvVariableSource_FromApp indicates that the env variable should be set to a value from another app
+	EnvVariableSource_FromApp EnvVariableSource = "app"
+)
+
+// EnvVariableDefinition is a struct containing information about how to set an env variable
+type EnvVariableDefinition struct {
+	Key     string
+	Source  EnvVariableSource
+	Value   EnvValueOptional
+	FromApp EnvVariableFromAppOptional
+}
+
+// EnvValueOptional is a struct wrapping an optional string value to be set as an env variable
+type EnvValueOptional struct {
+	// Value is the actual value of the env variable
+	Value string
+
+	IsSet bool
+}
+
+// EnvValueFromApp indicates which value to pull from the app
+type EnvValueFromApp string
+
+const (
+	// EnvValueFromApp_InternalDomain indicates that the internal domain should be pulled from the app
+	EnvValueFromApp_InternalDomain EnvValueFromApp = "internal_domain"
+	// EnvValueFromApp_PublicDomain indicates that the first found public domain should be pulled from the app
+	EnvValueFromApp_PublicDomain EnvValueFromApp = "public_domain"
+)
+
+// EnvVariableFromApp is a struct containing information about how to set an env variable from another app
+type EnvVariableFromApp struct {
+	// AppName is the name of the app to pull the value from
+	AppName string
+	// Value indicates which value to pull from the app
+	Value EnvValueFromApp
+	// ServiceName is the name of the service to pull the value from, if applicable
+	ServiceName string
+}
+
+// EnvVariableFromAppOptional is an optional value indicating how to resolve an env variable from another app
+type EnvVariableFromAppOptional struct {
+	Value EnvVariableFromApp
+
+	IsSet bool
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface for Env in order to support both a list of env variables and a map of env variables
+func (e *Env) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var asList []EnvVariableDefinition
+	if err := unmarshal(&asList); err == nil {
+		*e = asList
+		return nil
+	}
+
+	var asMap map[string]string
+	if err := unmarshal(&asMap); err != nil {
+		return err
+	}
+
+	for key, value := range asMap {
+		*e = append(*e, EnvVariableDefinition{
+			Key:    key,
+			Source: EnvVariableSource_Value,
+			Value: EnvValueOptional{
+				Value: value,
+				IsSet: true,
+			},
+		})
+	}
+
+	return nil
+}
+
+// MarshalYAML implements the yaml.Marshaler interface for Env in order to convert from the internal representation to the yaml representation
+func (e Env) MarshalYAML() (interface{}, error) {
+	var rawList []rawEnvVarDef
+
+	for _, def := range e {
+		switch def.Source {
+		case EnvVariableSource_Value:
+			rawList = append(rawList, rawEnvVarDef{
+				Key:   def.Key,
+				Value: def.Value.Value,
+			})
+		case EnvVariableSource_FromApp:
+			rawList = append(rawList, rawEnvVarDef{
+				Key: def.Key,
+				From: rawEnvVariableReference{
+					Source:      EnvVariableSource_FromApp,
+					Name:        def.FromApp.Value.AppName,
+					Value:       string(def.FromApp.Value.Value),
+					ServiceName: def.FromApp.Value.ServiceName,
+				},
+			})
+		}
+	}
+
+	return rawList, nil
+}
+
+// rawEnvVarDef is a struct used to unmarshal the yaml representation of an env variable
+// this represents the structure of an env variable in the yaml file
+type rawEnvVarDef struct {
+	Key   string                  `yaml:"key"`
+	Value string                  `yaml:"value,omitempty"`
+	From  rawEnvVariableReference `yaml:"from,omitempty"`
+}
+
+// rawEnvVariableReference is a struct used to unmarshal the yaml representation of an env variable reference
+type rawEnvVariableReference struct {
+	// source for the value. right now only "app" is supported
+	Source EnvVariableSource `yaml:"source,omitempty"`
+	// name of the app to get the value from
+	Name string `yaml:"name,omitempty"`
+	// value to pull from the app, right now only public_domain or internal_domain are supported
+	Value string `yaml:"value,omitempty"`
+	// name of the source to get the value from. only set if source is "app"
+	ServiceName string `yaml:"service,omitempty"`
+}
+
+// UnmarshalYAML implements the yaml.Unmarshaler interface for EnvVariableDefinition in order to support both a string value and a reference to another app
+func (def *EnvVariableDefinition) UnmarshalYAML(unmarshal func(interface{}) error) error {
+	var raw rawEnvVarDef
+	if err := unmarshal(&raw); err != nil {
+		return err
+	}
+
+	def.Key = raw.Key
+	if raw.Value != "" {
+		def.Source = EnvVariableSource_Value
+		def.Value = EnvValueOptional{
+			Value: raw.Value,
+			IsSet: true,
+		}
+	}
+
+	if !def.Value.IsSet {
+		switch raw.From.Source {
+		case EnvVariableSource_FromApp:
+			def.Source = EnvVariableSource_FromApp
+
+			value, err := fromAppValue(raw.From.Value)
+			if err != nil {
+				return fmt.Errorf("unknown app value provided: %w", err)
+			}
+
+			def.FromApp = EnvVariableFromAppOptional{
+				Value: EnvVariableFromApp{
+					AppName:     raw.From.Name,
+					Value:       value,
+					ServiceName: raw.From.ServiceName,
+				},
+				IsSet: true,
+			}
+		default:
+			return fmt.Errorf("invalid source %s for env variable", raw.From.Source)
+		}
+	}
+
+	return nil
+}
+
+// EnvVarFromAppToProto converts an env variable from app to the proto representation
+func EnvVarFromAppToProto(value EnvVariableFromApp) (*porterv1.EnvVariableFromApp, error) {
+	variable := &porterv1.EnvVariableFromApp{
+		AppName:     value.AppName,
+		ServiceName: value.ServiceName,
+	}
+
+	switch value.Value {
+	case EnvValueFromApp_InternalDomain:
+		variable.Value = porterv1.EnvValueFromApp_ENV_VALUE_FROM_APP_INTERNAL_DOMAIN
+	case EnvValueFromApp_PublicDomain:
+		variable.Value = porterv1.EnvValueFromApp_ENV_VALUE_FROM_APP_PUBLIC_DOMAIN
+	default:
+		return nil, fmt.Errorf("invalid value %s for env variable from app", value.Value)
+	}
+
+	return variable, nil
+}
+
+// EnvVarFromAppFromProto converts a proto env variable from app to the internal representation
+func EnvVarFromAppFromProto(value *porterv1.EnvVariableFromApp) (EnvVariableFromAppOptional, error) {
+	variable := EnvVariableFromAppOptional{
+		Value: EnvVariableFromApp{
+			AppName:     value.AppName,
+			ServiceName: value.ServiceName,
+		},
+		IsSet: true,
+	}
+
+	switch value.Value {
+	case porterv1.EnvValueFromApp_ENV_VALUE_FROM_APP_INTERNAL_DOMAIN:
+		variable.Value.Value = EnvValueFromApp_InternalDomain
+	case porterv1.EnvValueFromApp_ENV_VALUE_FROM_APP_PUBLIC_DOMAIN:
+		variable.Value.Value = EnvValueFromApp_PublicDomain
+	default:
+		return variable, fmt.Errorf("invalid value %s for env variable from app", value.Value)
+	}
+
+	return variable, nil
+}
+
+func fromAppValue(raw string) (EnvValueFromApp, error) {
+	switch raw {
+	case string(EnvValueFromApp_InternalDomain):
+		return EnvValueFromApp_InternalDomain, nil
+	case string(EnvValueFromApp_PublicDomain):
+		return EnvValueFromApp_PublicDomain, nil
+	default:
+		return "", fmt.Errorf("invalid value %s for env variable from app", raw)
+	}
+}

+ 42 - 7
internal/porter_app/v2/yaml.go

@@ -98,12 +98,12 @@ const (
 
 // PorterApp represents all the possible fields in a Porter YAML file
 type PorterApp struct {
-	Version  string            `yaml:"version,omitempty"`
-	Name     string            `yaml:"name"`
-	Services []Service         `yaml:"services"`
-	Image    *Image            `yaml:"image,omitempty"`
-	Build    *Build            `yaml:"build,omitempty"`
-	Env      map[string]string `yaml:"env,omitempty"`
+	Version  string    `yaml:"version,omitempty"`
+	Name     string    `yaml:"name"`
+	Services []Service `yaml:"services"`
+	Image    *Image    `yaml:"image,omitempty"`
+	Build    *Build    `yaml:"build,omitempty"`
+	Env      Env       `yaml:"env,omitempty"`
 
 	Predeploy    *Service      `yaml:"predeploy,omitempty"`
 	EnvGroups    []string      `yaml:"envGroups,omitempty"`
@@ -299,7 +299,42 @@ func ProtoFromApp(ctx context.Context, porterApp PorterApp) (*porterv1.PorterApp
 		})
 	}
 
-	return appProto, porterApp.Env, nil
+	envMap := make(map[string]string)
+	var envVariables []*porterv1.EnvVariable
+
+	for _, envVar := range porterApp.Env {
+		switch envVar.Source {
+		case EnvVariableSource_Value:
+			if !envVar.Value.IsSet {
+				return appProto, nil, telemetry.Error(ctx, span, nil, "no value set for env variable")
+			}
+
+			envMap[envVar.Key] = envVar.Value.Value
+		case EnvVariableSource_FromApp:
+			if !envVar.FromApp.IsSet {
+				return appProto, nil, telemetry.Error(ctx, span, nil, "no value set for env variable")
+			}
+
+			fromApp, err := EnvVarFromAppToProto(envVar.FromApp.Value)
+			if err != nil {
+				return appProto, nil, telemetry.Error(ctx, span, err, "error converting env variable from app to proto")
+			}
+
+			envVariables = append(envVariables, &porterv1.EnvVariable{
+				Key:    envVar.Key,
+				Source: porterv1.EnvVariableSource_ENV_VARIABLE_SOURCE_FROM_APP,
+				Definition: &porterv1.EnvVariable_FromApp{
+					FromApp: fromApp,
+				},
+			})
+		default:
+			return appProto, nil, telemetry.Error(ctx, span, nil, "invalid definition for env variable")
+		}
+	}
+
+	appProto.Env = envVariables
+
+	return appProto, envMap, nil
 }
 
 func protoEnumFromType(name string, service Service) porterv1.ServiceType {