yaml.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. package v2
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. "github.com/ghodss/yaml"
  8. porterv1 "github.com/porter-dev/api-contracts/generated/go/porter/v1"
  9. "github.com/porter-dev/porter/internal/telemetry"
  10. )
  11. // AppProtoFromYaml converts a Porter YAML file into a PorterApp proto object
  12. func AppProtoFromYaml(ctx context.Context, porterYamlBytes []byte, appName string) (*porterv1.PorterApp, error) {
  13. ctx, span := telemetry.NewSpan(ctx, "v2-app-proto-from-yaml")
  14. defer span.End()
  15. if porterYamlBytes == nil {
  16. return nil, telemetry.Error(ctx, span, nil, "porter yaml is nil")
  17. }
  18. porterYaml := &PorterYAML{}
  19. err := yaml.Unmarshal(porterYamlBytes, porterYaml)
  20. if err != nil {
  21. return nil, telemetry.Error(ctx, span, err, "error unmarshaling porter yaml")
  22. }
  23. // if the porter yaml is missing a name field, use the app name that is provided in the request
  24. if porterYaml.Name == "" {
  25. porterYaml.Name = appName
  26. }
  27. appProto := &porterv1.PorterApp{
  28. Name: porterYaml.Name,
  29. Env: porterYaml.Env,
  30. }
  31. if porterYaml.Build != nil {
  32. appProto.Build = &porterv1.Build{
  33. Context: porterYaml.Build.Context,
  34. Method: porterYaml.Build.Method,
  35. Builder: porterYaml.Build.Builder,
  36. Buildpacks: porterYaml.Build.Buildpacks,
  37. Dockerfile: porterYaml.Build.Dockerfile,
  38. }
  39. }
  40. if porterYaml.Image != nil {
  41. appProto.Image = &porterv1.AppImage{
  42. Repository: porterYaml.Image.Repository,
  43. Tag: porterYaml.Image.Tag,
  44. }
  45. }
  46. if porterYaml.Services == nil {
  47. return nil, telemetry.Error(ctx, span, nil, "porter yaml is missing services")
  48. }
  49. services := make(map[string]*porterv1.Service, 0)
  50. for name, service := range porterYaml.Services {
  51. serviceType, err := protoEnumFromType(name, service)
  52. if err != nil {
  53. return nil, telemetry.Error(ctx, span, err, "error getting service type")
  54. }
  55. serviceProto, err := serviceProtoFromConfig(service, serviceType)
  56. if err != nil {
  57. return nil, telemetry.Error(ctx, span, err, "error casting service config")
  58. }
  59. services[name] = serviceProto
  60. }
  61. appProto.Services = services
  62. if porterYaml.Predeploy != nil {
  63. predeployProto, err := serviceProtoFromConfig(*porterYaml.Predeploy, porterv1.ServiceType_SERVICE_TYPE_JOB)
  64. if err != nil {
  65. return nil, telemetry.Error(ctx, span, err, "error casting predeploy config")
  66. }
  67. appProto.Predeploy = predeployProto
  68. }
  69. return appProto, nil
  70. }
  71. // PorterYAML represents all the possible fields in a Porter YAML file
  72. type PorterYAML struct {
  73. Name string `yaml:"name"`
  74. Services map[string]Service `yaml:"services"`
  75. Image *Image `yaml:"image"`
  76. Build *Build `yaml:"build"`
  77. Env map[string]string `yaml:"env"`
  78. Predeploy *Service `yaml:"predeploy"`
  79. }
  80. // Build represents the build settings for a Porter app
  81. type Build struct {
  82. Context string `yaml:"context" validate:"dir"`
  83. Method string `yaml:"method" validate:"required,oneof=pack docker registry"`
  84. Builder string `yaml:"builder" validate:"required_if=Method pack"`
  85. Buildpacks []string `yaml:"buildpacks"`
  86. Dockerfile string `yaml:"dockerfile" validate:"required_if=Method docker"`
  87. }
  88. // Service represents a single service in a porter app
  89. type Service struct {
  90. Run string `yaml:"run"`
  91. Type string `yaml:"type" validate:"required, oneof=web worker job"`
  92. Instances int `yaml:"instances"`
  93. CpuCores float32 `yaml:"cpuCores"`
  94. RamMegabytes int `yaml:"ramMegabytes"`
  95. Port int `yaml:"port"`
  96. Autoscaling *AutoScaling `yaml:"autoscaling,omitempty" validate:"excluded_if=Type job"`
  97. Domains []Domains `yaml:"domains" validate:"excluded_unless=Type web"`
  98. HealthCheck *HealthCheck `yaml:"healthCheck,omitempty" validate:"excluded_unless=Type web"`
  99. AllowConcurrent bool `yaml:"allowConcurrent" validate:"excluded_unless=Type job"`
  100. Cron string `yaml:"cron" validate:"excluded_unless=Type job"`
  101. Private bool `yaml:"private" validate:"excluded_unless=Type web"`
  102. }
  103. // AutoScaling represents the autoscaling settings for web services
  104. type AutoScaling struct {
  105. Enabled bool `yaml:"enabled"`
  106. MinInstances int `yaml:"minInstances"`
  107. MaxInstances int `yaml:"maxInstances"`
  108. CpuThresholdPercent int `yaml:"cpuThresholdPercent"`
  109. MemoryThresholdPercent int `yaml:"memoryThresholdPercent"`
  110. }
  111. // Domains are the custom domains for a web service
  112. type Domains struct {
  113. Name string `yaml:"name"`
  114. }
  115. // HealthCheck is the health check settings for a web service
  116. type HealthCheck struct {
  117. Enabled bool `yaml:"enabled"`
  118. HttpPath string `yaml:"httpPath"`
  119. }
  120. // Image is the repository and tag for an app's build image
  121. type Image struct {
  122. Repository string `yaml:"repository"`
  123. Tag string `yaml:"tag"`
  124. }
  125. func protoEnumFromType(name string, service Service) (porterv1.ServiceType, error) {
  126. var serviceType porterv1.ServiceType
  127. if service.Type != "" {
  128. if service.Type == "web" {
  129. return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
  130. }
  131. if service.Type == "worker" {
  132. return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
  133. }
  134. if service.Type == "job" {
  135. return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
  136. }
  137. return serviceType, fmt.Errorf("invalid service type '%s'", service.Type)
  138. }
  139. if strings.Contains(name, "web") {
  140. return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
  141. }
  142. if strings.Contains(name, "wkr") {
  143. return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
  144. }
  145. if strings.Contains(name, "job") {
  146. return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
  147. }
  148. return serviceType, errors.New("no type provided and could not parse service type from name")
  149. }
  150. func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (*porterv1.Service, error) {
  151. serviceProto := &porterv1.Service{
  152. Run: service.Run,
  153. Type: serviceType,
  154. Instances: int32(service.Instances),
  155. CpuCores: service.CpuCores,
  156. RamMegabytes: int32(service.RamMegabytes),
  157. Port: int32(service.Port),
  158. }
  159. switch serviceType {
  160. default:
  161. return nil, fmt.Errorf("invalid service type '%s'", serviceType)
  162. case porterv1.ServiceType_SERVICE_TYPE_UNSPECIFIED:
  163. return nil, errors.New("Service type unspecified")
  164. case porterv1.ServiceType_SERVICE_TYPE_WEB:
  165. webConfig := &porterv1.WebServiceConfig{}
  166. var autoscaling *porterv1.Autoscaling
  167. if service.Autoscaling != nil {
  168. autoscaling = &porterv1.Autoscaling{
  169. Enabled: service.Autoscaling.Enabled,
  170. MinInstances: int32(service.Autoscaling.MinInstances),
  171. MaxInstances: int32(service.Autoscaling.MaxInstances),
  172. CpuThresholdPercent: int32(service.Autoscaling.CpuThresholdPercent),
  173. MemoryThresholdPercent: int32(service.Autoscaling.MemoryThresholdPercent),
  174. }
  175. }
  176. webConfig.Autoscaling = autoscaling
  177. var healthCheck *porterv1.HealthCheck
  178. if service.HealthCheck != nil {
  179. healthCheck = &porterv1.HealthCheck{
  180. Enabled: service.HealthCheck.Enabled,
  181. HttpPath: service.HealthCheck.HttpPath,
  182. }
  183. }
  184. webConfig.HealthCheck = healthCheck
  185. domains := make([]*porterv1.Domain, 0)
  186. for _, domain := range service.Domains {
  187. domains = append(domains, &porterv1.Domain{
  188. Name: domain.Name,
  189. })
  190. }
  191. webConfig.Domains = domains
  192. webConfig.Private = service.Private
  193. serviceProto.Config = &porterv1.Service_WebConfig{
  194. WebConfig: webConfig,
  195. }
  196. case porterv1.ServiceType_SERVICE_TYPE_WORKER:
  197. workerConfig := &porterv1.WorkerServiceConfig{}
  198. var autoscaling *porterv1.Autoscaling
  199. if service.Autoscaling != nil {
  200. autoscaling = &porterv1.Autoscaling{
  201. Enabled: service.Autoscaling.Enabled,
  202. MinInstances: int32(service.Autoscaling.MinInstances),
  203. MaxInstances: int32(service.Autoscaling.MaxInstances),
  204. CpuThresholdPercent: int32(service.Autoscaling.CpuThresholdPercent),
  205. MemoryThresholdPercent: int32(service.Autoscaling.MemoryThresholdPercent),
  206. }
  207. }
  208. workerConfig.Autoscaling = autoscaling
  209. serviceProto.Config = &porterv1.Service_WorkerConfig{
  210. WorkerConfig: workerConfig,
  211. }
  212. case porterv1.ServiceType_SERVICE_TYPE_JOB:
  213. jobConfig := &porterv1.JobServiceConfig{
  214. AllowConcurrent: service.AllowConcurrent,
  215. Cron: service.Cron,
  216. }
  217. serviceProto.Config = &porterv1.Service_JobConfig{
  218. JobConfig: jobConfig,
  219. }
  220. }
  221. return serviceProto, nil
  222. }