Răsfoiți Sursa

Merge branch 'master' into stacks-v2-fix-predeploy

Feroze Mohideen 2 ani în urmă
părinte
comite
a65d29b42f

+ 1 - 7
api/server/handlers/gitinstallation/get_porter_yaml.go

@@ -103,13 +103,7 @@ func (c *GithubGetPorterYamlHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
 	}
 
 	if project.ValidateApplyV2 {
-		if parsed.Version == nil {
-			err = telemetry.Error(ctx, span, nil, "v2 porter yaml is required")
-			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
-			return
-		}
-
-		if *parsed.Version != "v2" {
+		if parsed.Version != nil && *parsed.Version != "v2" {
 			err = telemetry.Error(ctx, span, nil, "porter YAML version is not supported")
 			c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusBadRequest))
 			return

+ 3 - 2
api/server/handlers/porter_app/parse_yaml.go

@@ -37,6 +37,7 @@ func NewParsePorterYAMLToProtoHandler(
 // ParsePorterYAMLToProtoRequest is the request object for the /apps/parse endpoint
 type ParsePorterYAMLToProtoRequest struct {
 	B64Yaml string `json:"b64_yaml"`
+	AppName string `json:"app_name"`
 }
 
 // ParsePorterYAMLToProtoResponse is the response object for the /apps/parse endpoint
@@ -53,7 +54,7 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http
 
 	if !project.ValidateApplyV2 {
 		err := telemetry.Error(ctx, span, nil, "project does not have apply v2 enabled")
-		c.HandleAPIError(w, r, apierrors.NewErrForbidden(err))
+		c.HandleAPIError(w, r, apierrors.NewErrPassThroughToClient(err, http.StatusForbidden))
 		return
 	}
 
@@ -82,7 +83,7 @@ func (c *ParsePorterYAMLToProtoHandler) ServeHTTP(w http.ResponseWriter, r *http
 		return
 	}
 
-	appProto, err := porter_app.ParseYAML(ctx, yaml)
+	appProto, 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))

+ 18 - 3
internal/porter_app/parse.go

@@ -3,6 +3,7 @@ package porter_app
 import (
 	"context"
 
+	v1 "github.com/porter-dev/porter/internal/porter_app/v1"
 	v2 "github.com/porter-dev/porter/internal/porter_app/v2"
 
 	"sigs.k8s.io/yaml"
@@ -17,15 +18,17 @@ type PorterYamlVersion string
 const (
 	// PorterYamlVersion_V2 is the v2 version of the porter yaml
 	PorterYamlVersion_V2 PorterYamlVersion = "v2"
+	// PorterYamlVersion_V1 is the v1, legacy version of the porter yaml
+	PorterYamlVersion_V1 PorterYamlVersion = "v1stack"
 )
 
 // ParseYAML converts a Porter YAML file into a PorterApp proto object
-func ParseYAML(ctx context.Context, porterYaml []byte) (*porterv1.PorterApp, error) {
+func ParseYAML(ctx context.Context, porterYaml []byte, appName string) (*porterv1.PorterApp, error) {
 	ctx, span := telemetry.NewSpan(ctx, "porter-app-parse-yaml")
 	defer span.End()
 
 	if porterYaml == nil {
-		return nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml input is nil")
 	}
 
 	version := &yamlVersion{}
@@ -35,18 +38,30 @@ func ParseYAML(ctx context.Context, porterYaml []byte) (*porterv1.PorterApp, err
 	}
 
 	var appProto *porterv1.PorterApp
+
 	switch version.Version {
 	case PorterYamlVersion_V2:
 		appProto, err = v2.AppProtoFromYaml(ctx, porterYaml)
 		if err != nil {
 			return nil, 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, "":
+		if appName == "" {
+			return nil, telemetry.Error(ctx, span, nil, "v1 porter yaml requires externally-provided app name")
+		}
+		appProto, err = v1.AppProtoFromYaml(ctx, porterYaml, appName)
+		if err != nil {
+			return nil, telemetry.Error(ctx, span, err, "error converting v1 yaml to proto")
+		}
 	default:
 		return nil, telemetry.Error(ctx, span, nil, "porter yaml version not supported")
 	}
 
 	if appProto == nil {
-		return nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml output is nil")
 	}
 
 	return appProto, nil

+ 77 - 1
internal/porter_app/parse_test.go

@@ -20,6 +20,7 @@ func TestParseYAML(t *testing.T) {
 		want               *porterv1.PorterApp
 	}{
 		{"v2_input_nobuild", result_nobuild},
+		{"v1_input_no_build_no_image", v1_result_nobuild_no_image},
 	}
 
 	for _, tt := range tests {
@@ -29,7 +30,7 @@ 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, err := ParseYAML(context.Background(), want)
+			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)
@@ -116,6 +117,81 @@ var result_nobuild = &porterv1.PorterApp{
 	},
 }
 
+var v1_result_nobuild_no_image = &porterv1.PorterApp{
+	Name: "test-app",
+	Services: map[string]*porterv1.Service{
+		"example-job": {
+			Run:          "echo 'hello world'",
+			CpuCores:     0.1,
+			RamMegabytes: 256,
+			Config: &porterv1.Service_JobConfig{
+				JobConfig: &porterv1.JobServiceConfig{
+					AllowConcurrent: true,
+					Cron:            "*/10 * * * *",
+				},
+			},
+			Type: 3,
+		},
+		"example-wkr": {
+			Run:          "echo 'work'",
+			Instances:    1,
+			Port:         80,
+			CpuCores:     0.1,
+			RamMegabytes: 256,
+			Config: &porterv1.Service_WorkerConfig{
+				WorkerConfig: &porterv1.WorkerServiceConfig{
+					Autoscaling: nil,
+				},
+			},
+			Type: 2,
+		},
+		"example-web": {
+			Run:          "node index.js",
+			Instances:    0,
+			Port:         8080,
+			CpuCores:     0.1,
+			RamMegabytes: 256,
+			Config: &porterv1.Service_WebConfig{
+				WebConfig: &porterv1.WebServiceConfig{
+					Autoscaling: &porterv1.Autoscaling{
+						Enabled:                true,
+						MinInstances:           1,
+						MaxInstances:           3,
+						CpuThresholdPercent:    60,
+						MemoryThresholdPercent: 60,
+					},
+					Domains: []*porterv1.Domain{
+						{
+							Name: "test1.example.com",
+						},
+						{
+							Name: "test2.example.com",
+						},
+					},
+					HealthCheck: &porterv1.HealthCheck{
+						Enabled:  true,
+						HttpPath: "/healthz",
+					},
+				},
+			},
+			Type: 1,
+		},
+	},
+	Env: map[string]string{
+		"PORT":     "8080",
+		"NODE_ENV": "production",
+	},
+	Predeploy: &porterv1.Service{
+		Run:          "ls",
+		Instances:    0,
+		Port:         0,
+		CpuCores:     0,
+		RamMegabytes: 0,
+		Config:       &porterv1.Service_JobConfig{},
+		Type:         3,
+	},
+}
+
 func diffProtoWithFailTest(t *testing.T, is *is.I, want, got *porterv1.PorterApp) {
 	t.Helper()
 

+ 95 - 0
internal/porter_app/testdata/v1_input_no_build_no_image.yaml

@@ -0,0 +1,95 @@
+version: v1stack
+apps:
+  example-job:
+    type: job
+    run: echo 'hello world'
+    config:
+      allowConcurrent: true
+      resources:
+        requests:
+          cpu: 100m
+          memory: 256Mi
+      schedule:
+        enabled: true
+        value: '*/10 * * * *'
+      paused: true
+      cloudsql:
+        enabled: false
+        connectionName: ''
+        dbPort: '5432'
+        serviceAccountJSON: ''
+  example-wkr:
+    type: worker
+    run: "echo 'work'"
+    config:
+      replicaCount: '1'
+      container:
+        port: '80'
+      resources:
+        requests:
+          cpu: 100m
+          memory: 256Mi
+      autoscaling:
+        enabled: false
+        minReplicas: '1'
+        maxReplicas: '10'
+        targetCPUUtilizationPercentage: '50'
+        targetMemoryUtilizationPercentage: '50'
+      cloudsql:
+        enabled: false
+        connectionName: ''
+        dbPort: '5432'
+        serviceAccountJSON: ''
+  example-web:
+    type: web
+    run: node index.js
+    config:
+      replicaCount: '0'
+      resources:
+        requests:
+          cpu: 100m
+          memory: 256Mi
+      container:
+        port: '8080'
+      autoscaling:
+        enabled: true
+        minReplicas: '1'
+        maxReplicas: '3'
+        targetCPUUtilizationPercentage: '60'
+        targetMemoryUtilizationPercentage: '60'
+      ingress:
+        enabled: true
+        custom_domain: true
+        hosts:
+          - test1.example.com
+          - test2.example.com
+        porter_hosts: []
+        annotations:
+      service:
+        port: '8080'
+      health:
+        startupProbe:
+          enabled: false
+          failureThreshold: '3'
+          path: /startupz
+          periodSeconds: '5'
+        readinessProbe:
+          enabled: true
+          failureThreshold: '3'
+          path: /healthz
+          initialDelaySeconds: '0'
+        livenessProbe:
+          enabled: true
+          failureThreshold: '3'
+          path: /healthz
+          periodSeconds: '5'
+      cloudsql:
+        enabled: false
+        connectionName: ''
+        dbPort: '5432'
+        serviceAccountJSON: ''
+release:
+  run: ls
+env:
+  PORT: '8080'
+  NODE_ENV: 'production'

+ 117 - 0
internal/porter_app/v1/types.go

@@ -0,0 +1,117 @@
+package v1
+
+// ServiceConfig contains the configuration exposed to users in v1stack porter.yaml
+type ServiceConfig struct {
+	Autoscaling      *Autoscaling      `yaml:"autoscaling,omitempty" validate:"excluded_if=Type job"`
+	Container        Container         `yaml:"container"`
+	Health           *Health           `yaml:"health,omitempty" validate:"excluded_unless=Type web"`
+	Ingress          Ingress           `yaml:"ingress"`
+	ReplicaCount     string            `yaml:"replicaCount"`
+	Resources        Resources         `yaml:"resources"`
+	Service          KubernetesService `yaml:"service"`
+	AllowConcurrency bool              `yaml:"allowConcurrent" validate:"excluded_unless=Type job"`
+	Schedule         Schedule          `yaml:"schedule" validate:"excluded_unless=Type job"`
+}
+
+// Schedule contains all configuration for job schedules
+type Schedule struct {
+	Enabled bool `yaml:"enabled"`
+	// Value is the cron schedule
+	Value string `yaml:"value,omitempty"`
+}
+
+// Autoscaling contains all configuration for autoscaling in a web/worker chart
+type Autoscaling struct {
+	Enabled                           bool   `yaml:"enabled"`
+	MaxReplicas                       string `yaml:"maxReplicas"`
+	MinReplicas                       string `yaml:"minReplicas"`
+	TargetCPUUtilizationPercentage    string `yaml:"targetCPUUtilizationPercentage"`
+	TargetMemoryUtilizationPercentage string `yaml:"targetMemoryUtilizationPercentage"`
+}
+
+// Container contains all configuration for containers
+type Container struct {
+	Port string `yaml:"port"`
+}
+
+// Health contains user-configurable health probes
+type Health struct {
+	// LivenessProbe checks whether a container should be considered healthy
+	LivenessProbe LivenessProbe `yaml:"livenessProbe"`
+	// ReadinessProbe checks whether a container should be considered ready to receive traffic
+	ReadinessProbe ReadinessProbe `yaml:"readinessProbe"`
+}
+
+// LivenessProbe contains user-configurable values for a liveness probe
+type LivenessProbe struct {
+	Enabled bool `yaml:"enabled"`
+	// Path is the endpoint path to use for the probe
+	Path string `yaml:"path"`
+}
+
+// ReadinessProbe contains user-configurable values for a readiness probe
+type ReadinessProbe struct {
+	Enabled bool `yaml:"enabled"`
+	// Path is the endpoint path to use for the probe
+	Path string `yaml:"path"`
+}
+
+// Ingress contains configuration for ingress used by web charts
+type Ingress struct {
+	// Enabled specifies whether or not to use an ingress
+	Enabled bool `yaml:"enabled"`
+	// Hosts specifies the domains to include in the routing rules
+	Hosts []string `yaml:"hosts"`
+	// PorterHosts specifies the porter domains to include in the routing rules
+	PorterHosts []string `yaml:"porter_hosts"`
+	// Annotations specifies annotations to add to the ingress
+	Annotations map[string]string `yaml:"annotations"`
+}
+
+// Resources is a wrapper over requests
+type Resources struct {
+	// Requests contains configuration for resource requests
+	Requests Requests `yaml:"requests"`
+}
+
+// Requests contains configuration for resource requests
+type Requests struct {
+	// Cpu is the cpu request (e.g. 100m - m for millicores)
+	Cpu string `yaml:"cpu"`
+	// Memory is the memory request (e.g. 100Mi - Mi for mebibytes)
+	Memory string `yaml:"memory"`
+}
+
+// KubernetesService contains configuration for exposing services
+type KubernetesService struct {
+	// Port is the port to expose the service on. This port should match the port in the container.
+	Port string `yaml:"port"`
+}
+
+// PorterYAML represents the accepted top-level fields in a porter.yaml
+type PorterYAML struct {
+	Version  *string            `yaml:"version"`
+	Build    *Build             `yaml:"build"`
+	Env      map[string]string  `yaml:"env"`
+	Apps     map[string]Service `yaml:"apps" validate:"required_without=Services"`
+	Services map[string]Service `yaml:"services" validate:"required_without=Apps"`
+
+	Release *Service `yaml:"release"`
+}
+
+// Build represents the build settings for a Porter app
+type Build struct {
+	Context    string   `yaml:"context" validate:"dir"`
+	Method     string   `yaml:"method" validate:"required,oneof=pack docker registry"`
+	Builder    string   `yaml:"builder" validate:"required_if=Method pack"`
+	Buildpacks []string `yaml:"buildpacks"`
+	Dockerfile string   `yaml:"dockerfile" validate:"required_if=Method docker"`
+	Image      string   `yaml:"image" validate:"required_if=Method registry"`
+}
+
+// Service represents a service in a Porter app
+type Service struct {
+	Run    string        `yaml:"run"`
+	Config ServiceConfig `yaml:"config"`
+	Type   string        `yaml:"type" validate:"required, oneof=web worker job"`
+}

+ 367 - 0
internal/porter_app/v1/yaml.go

@@ -0,0 +1,367 @@
+package v1
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"math"
+	"strconv"
+	"strings"
+
+	porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
+	"github.com/porter-dev/porter/internal/telemetry"
+
+	"gopkg.in/yaml.v2"
+)
+
+// AppProtoFromYaml converts an old version Porter YAML file into a PorterApp proto object
+func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte, appName string) (*porterv1.PorterApp, error) {
+	ctx, span := telemetry.NewSpan(ctx, "v1-app-proto-from-yaml")
+	defer span.End()
+
+	if appName == "" {
+		return nil, telemetry.Error(ctx, span, nil, "app name is empty")
+	}
+	telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "app-name", Value: appName})
+
+	if porterYamlBytes == nil {
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
+	}
+
+	porterYaml := &PorterYAML{}
+	err := yaml.Unmarshal(porterYamlBytes, porterYaml)
+	if err != nil {
+		return nil, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml")
+	}
+
+	appProto := &porterv1.PorterApp{
+		Name: appName,
+		Env:  porterYaml.Env,
+	}
+
+	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.Build != nil && porterYaml.Build.Image != "" {
+		imageSpl := strings.Split(porterYaml.Build.Image, ":")
+		if len(imageSpl) == 2 {
+			appProto.Image = &porterv1.AppImage{
+				Repository: imageSpl[0],
+				Tag:        imageSpl[1],
+			}
+		} else {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "image", Value: porterYaml.Build.Image})
+			return nil, telemetry.Error(ctx, span, err, "error parsing image")
+		}
+	}
+
+	if porterYaml.Apps != nil && porterYaml.Services != nil {
+		return nil, telemetry.Error(ctx, span, nil, "'apps' and 'services' are synonymous but both were defined")
+	}
+	var services map[string]Service
+	if porterYaml.Apps != nil {
+		services = porterYaml.Apps
+	}
+
+	if porterYaml.Services != nil {
+		services = porterYaml.Services
+	}
+
+	if services == nil {
+		return nil, telemetry.Error(ctx, span, nil, "porter yaml is missing services")
+	}
+
+	serviceProtoMap := make(map[string]*porterv1.Service, 0)
+	for name, service := range services {
+		serviceType, err := protoEnumFromType(name, service)
+		if err != nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "failing-service-name", Value: name})
+			return nil, telemetry.Error(ctx, span, err, "error getting service type")
+		}
+
+		serviceProto, err := serviceProtoFromConfig(service, serviceType)
+		if err != nil {
+			telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "failing-service-name", Value: name})
+			return nil, telemetry.Error(ctx, span, err, "error casting service config")
+		}
+
+		serviceProtoMap[name] = serviceProto
+	}
+	appProto.Services = serviceProtoMap
+
+	if porterYaml.Release != nil {
+		predeployProto, err := serviceProtoFromConfig(*porterYaml.Release, porterv1.ServiceType_SERVICE_TYPE_JOB)
+		if err != nil {
+			return nil, telemetry.Error(ctx, span, err, "error casting predeploy config")
+		}
+		appProto.Predeploy = predeployProto
+	}
+
+	return appProto, nil
+}
+
+func protoEnumFromType(name string, service Service) (porterv1.ServiceType, error) {
+	var serviceType porterv1.ServiceType
+
+	if service.Type != "" {
+		if service.Type == "web" {
+			return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
+		}
+		if service.Type == "worker" {
+			return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
+		}
+		if service.Type == "job" {
+			return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
+		}
+
+		return serviceType, fmt.Errorf("invalid service type '%s'", service.Type)
+	}
+
+	if strings.Contains(name, "web") {
+		return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
+	}
+
+	if strings.Contains(name, "wkr") {
+		return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
+	}
+
+	if strings.Contains(name, "job") {
+		return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
+	}
+
+	if name == "release" {
+		return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
+	}
+
+	return serviceType, errors.New("no type provided and could not parse service type from name")
+}
+
+func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (*porterv1.Service, error) {
+	serviceProto := &porterv1.Service{
+		Run:  service.Run,
+		Type: serviceType,
+	}
+
+	// if the revision number cannot be converted, it will default to 0
+	replicaCount, _ := strconv.Atoi(service.Config.ReplicaCount)
+	if replicaCount < math.MinInt32 || replicaCount > math.MaxInt32 {
+		return nil, fmt.Errorf("replica count is out of range of int32")
+	}
+	// nolint:gosec
+	serviceProto.Instances = int32(replicaCount)
+
+	if service.Config.Resources.Requests.Cpu != "" {
+		cpuCoresStr := service.Config.Resources.Requests.Cpu
+		if !strings.HasSuffix(cpuCoresStr, "m") {
+			return nil, fmt.Errorf("cpu is not in millicores")
+		}
+
+		cpuCoresStr = strings.TrimSuffix(cpuCoresStr, "m")
+		cpuCoresFloat64, err := strconv.ParseFloat(cpuCoresStr, 32)
+		if err != nil {
+			return nil, fmt.Errorf("cpu is not a float")
+		}
+		serviceProto.CpuCores = float32(cpuCoresFloat64) / 1000
+	}
+
+	if service.Config.Resources.Requests.Memory != "" {
+		memoryStr := service.Config.Resources.Requests.Memory
+		if !strings.HasSuffix(memoryStr, "Mi") {
+			return nil, fmt.Errorf("memory is not in Mi")
+		}
+
+		memoryStr = strings.TrimSuffix(memoryStr, "Mi")
+		memoryFloat64, err := strconv.ParseFloat(memoryStr, 32)
+		if err != nil {
+			return nil, fmt.Errorf("memory is not a float")
+		}
+		// nolint:gosec
+		serviceProto.RamMegabytes = int32(memoryFloat64)
+	}
+
+	if service.Config.Container.Port != "" && service.Config.Service.Port != "" && service.Config.Container.Port != service.Config.Service.Port {
+		return nil, errors.New("container port and service port do not match")
+	}
+	if service.Config.Container.Port != "" {
+		port, err := strconv.Atoi(service.Config.Container.Port)
+		if err != nil {
+			return nil, fmt.Errorf("container port cannot be converted to int: %w", err)
+		}
+		if port < math.MinInt32 || port > math.MaxInt32 {
+			return nil, fmt.Errorf("port is out of range of int32")
+		}
+		// nolint:gosec
+		serviceProto.Port = int32(port)
+	}
+	if service.Config.Service.Port != "" {
+		port, err := strconv.Atoi(service.Config.Service.Port)
+		if err != nil {
+			return nil, fmt.Errorf("service port cannot be converted to int: %w", err)
+		}
+		if port < math.MinInt32 || port > math.MaxInt32 {
+			return nil, fmt.Errorf("port is out of range of int32")
+		}
+		// nolint:gosec
+		serviceProto.Port = int32(port)
+	}
+
+	switch serviceType {
+	default:
+		return nil, fmt.Errorf("invalid service type '%s'", serviceType)
+	case porterv1.ServiceType_SERVICE_TYPE_UNSPECIFIED:
+		return nil, errors.New("KubernetesService type unspecified")
+	case porterv1.ServiceType_SERVICE_TYPE_WEB:
+		webConfig, err := webConfigProtoFromConfig(service)
+		if err != nil {
+			return nil, fmt.Errorf("error converting web config: %w", err)
+		}
+
+		serviceProto.Config = &porterv1.Service_WebConfig{
+			WebConfig: webConfig,
+		}
+	case porterv1.ServiceType_SERVICE_TYPE_WORKER:
+		workerConfig, err := workerConfigProtoFromConfig(service)
+		if err != nil {
+			return nil, fmt.Errorf("error converting worker config: %w", err)
+		}
+
+		serviceProto.Config = &porterv1.Service_WorkerConfig{
+			WorkerConfig: workerConfig,
+		}
+	case porterv1.ServiceType_SERVICE_TYPE_JOB:
+		jobConfig := &porterv1.JobServiceConfig{
+			AllowConcurrent: service.Config.AllowConcurrency,
+			Cron:            service.Config.Schedule.Value,
+		}
+
+		serviceProto.Config = &porterv1.Service_JobConfig{
+			JobConfig: jobConfig,
+		}
+	}
+
+	return serviceProto, nil
+}
+
+func workerConfigProtoFromConfig(service Service) (*porterv1.WorkerServiceConfig, error) {
+	workerConfig := &porterv1.WorkerServiceConfig{}
+
+	var autoscaling *porterv1.Autoscaling
+	if service.Config.Autoscaling != nil && service.Config.Autoscaling.Enabled {
+		autoscaling = &porterv1.Autoscaling{
+			Enabled: service.Config.Autoscaling.Enabled,
+		}
+		minReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MinReplicas)
+		if minReplicas < math.MinInt32 || minReplicas > math.MaxInt32 {
+			return nil, fmt.Errorf("minReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MinInstances = int32(minReplicas)
+		maxReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MaxReplicas)
+		if maxReplicas < math.MinInt32 || maxReplicas > math.MaxInt32 {
+			return nil, fmt.Errorf("maxReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MaxInstances = int32(maxReplicas)
+		cpuThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetCPUUtilizationPercentage)
+		if cpuThresholdPercent < math.MinInt32 || cpuThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("cpuThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.CpuThresholdPercent = int32(cpuThresholdPercent)
+		memoryThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetMemoryUtilizationPercentage)
+		if memoryThresholdPercent < math.MinInt32 || memoryThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("memoryThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MemoryThresholdPercent = int32(memoryThresholdPercent)
+	}
+	workerConfig.Autoscaling = autoscaling
+
+	return workerConfig, nil
+}
+
+func webConfigProtoFromConfig(service Service) (*porterv1.WebServiceConfig, error) {
+	webConfig := &porterv1.WebServiceConfig{}
+
+	var autoscaling *porterv1.Autoscaling
+	if service.Config.Autoscaling != nil && service.Config.Autoscaling.Enabled {
+		autoscaling = &porterv1.Autoscaling{
+			Enabled: service.Config.Autoscaling.Enabled,
+		}
+		minReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MinReplicas)
+		if minReplicas < math.MinInt32 || minReplicas > math.MaxInt32 {
+			return nil, errors.New("minReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MinInstances = int32(minReplicas)
+		maxReplicas, _ := strconv.Atoi(service.Config.Autoscaling.MaxReplicas)
+		if maxReplicas < math.MinInt32 || maxReplicas > math.MaxInt32 {
+			return nil, errors.New("maxReplicas is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MaxInstances = int32(maxReplicas)
+		cpuThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetCPUUtilizationPercentage)
+		if cpuThresholdPercent < math.MinInt32 || cpuThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("cpuThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.CpuThresholdPercent = int32(cpuThresholdPercent)
+		memoryThresholdPercent, _ := strconv.Atoi(service.Config.Autoscaling.TargetMemoryUtilizationPercentage)
+		if memoryThresholdPercent < math.MinInt32 || memoryThresholdPercent > math.MaxInt32 {
+			return nil, fmt.Errorf("memoryThresholdPercent is out of range of int32")
+		}
+		// nolint:gosec
+		autoscaling.MemoryThresholdPercent = int32(memoryThresholdPercent)
+	}
+	webConfig.Autoscaling = autoscaling
+
+	var healthCheck *porterv1.HealthCheck
+	// note that we are only reading from the readiness probe config, since readiness and liveness share the same config now
+	if service.Config.Health != nil {
+		health := service.Config.Health
+		if health.ReadinessProbe.Enabled && health.LivenessProbe.Enabled && health.ReadinessProbe.Path != health.LivenessProbe.Path {
+			return nil, errors.New("liveness and readiness probes must have the same path")
+		}
+		if health.ReadinessProbe.Enabled {
+			healthCheck = &porterv1.HealthCheck{
+				Enabled:  service.Config.Health.ReadinessProbe.Enabled,
+				HttpPath: service.Config.Health.ReadinessProbe.Path,
+			}
+		} else if health.LivenessProbe.Enabled {
+			healthCheck = &porterv1.HealthCheck{
+				Enabled:  service.Config.Health.LivenessProbe.Enabled,
+				HttpPath: service.Config.Health.LivenessProbe.Path,
+			}
+		}
+	}
+
+	webConfig.HealthCheck = healthCheck
+
+	domains := make([]*porterv1.Domain, 0)
+	for _, domain := range service.Config.Ingress.Hosts {
+		hostName := domain
+		domains = append(domains, &porterv1.Domain{
+			Name: hostName,
+		})
+	}
+	for _, domain := range service.Config.Ingress.PorterHosts {
+		hostName := domain
+		domains = append(domains, &porterv1.Domain{
+			Name: hostName,
+		})
+	}
+	if service.Config.Ingress.Annotations != nil && len(service.Config.Ingress.Annotations) > 0 {
+		return nil, errors.New("annotations are not supported")
+	}
+	webConfig.Domains = domains
+	webConfig.Private = !service.Config.Ingress.Enabled
+
+	return webConfig, nil
+}