|
|
@@ -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
|
|
|
+}
|