yaml.go 8.1 KB

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