yaml.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. }
  98. // AutoScaling represents the autoscaling settings for web services
  99. type AutoScaling struct {
  100. Enabled bool `yaml:"enabled"`
  101. MinInstances int `yaml:"minInstances"`
  102. MaxInstances int `yaml:"maxInstances"`
  103. CpuThresholdPercent int `yaml:"cpuThresholdPercent"`
  104. MemoryThresholdPercent int `yaml:"memoryThresholdPercent"`
  105. }
  106. // Domains are the custom domains for a web service
  107. type Domains struct {
  108. Name string `yaml:"name"`
  109. }
  110. // HealthCheck is the health check settings for a web service
  111. type HealthCheck struct {
  112. Enabled bool `yaml:"enabled"`
  113. HttpPath string `yaml:"httpPath"`
  114. }
  115. // Image is the repository and tag for an app's build image
  116. type Image struct {
  117. Repository string `yaml:"repository"`
  118. Tag string `yaml:"tag"`
  119. }
  120. func protoEnumFromType(name string, service Service) (porterv1.ServiceType, error) {
  121. var serviceType porterv1.ServiceType
  122. if service.Type != "" {
  123. if service.Type == "web" {
  124. return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
  125. }
  126. if service.Type == "worker" {
  127. return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
  128. }
  129. if service.Type == "job" {
  130. return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
  131. }
  132. return serviceType, fmt.Errorf("invalid service type '%s'", service.Type)
  133. }
  134. if strings.Contains(name, "web") {
  135. return porterv1.ServiceType_SERVICE_TYPE_WEB, nil
  136. }
  137. if strings.Contains(name, "wkr") {
  138. return porterv1.ServiceType_SERVICE_TYPE_WORKER, nil
  139. }
  140. if strings.Contains(name, "job") {
  141. return porterv1.ServiceType_SERVICE_TYPE_JOB, nil
  142. }
  143. return serviceType, errors.New("no type provided and could not parse service type from name")
  144. }
  145. func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (*porterv1.Service, error) {
  146. serviceProto := &porterv1.Service{
  147. Run: service.Run,
  148. Type: serviceType,
  149. Instances: int32(service.Instances),
  150. CpuCores: service.CpuCores,
  151. RamMegabytes: int32(service.RamMegabytes),
  152. Port: int32(service.Port),
  153. }
  154. switch serviceType {
  155. default:
  156. return nil, fmt.Errorf("invalid service type '%s'", serviceType)
  157. case porterv1.ServiceType_SERVICE_TYPE_UNSPECIFIED:
  158. return nil, errors.New("Service type unspecified")
  159. case porterv1.ServiceType_SERVICE_TYPE_WEB:
  160. webConfig := &porterv1.WebServiceConfig{}
  161. var autoscaling *porterv1.Autoscaling
  162. if service.Autoscaling != nil {
  163. autoscaling = &porterv1.Autoscaling{
  164. Enabled: service.Autoscaling.Enabled,
  165. MinInstances: int32(service.Autoscaling.MinInstances),
  166. MaxInstances: int32(service.Autoscaling.MaxInstances),
  167. CpuThresholdPercent: int32(service.Autoscaling.CpuThresholdPercent),
  168. MemoryThresholdPercent: int32(service.Autoscaling.MemoryThresholdPercent),
  169. }
  170. }
  171. webConfig.Autoscaling = autoscaling
  172. var healthCheck *porterv1.HealthCheck
  173. if service.HealthCheck != nil {
  174. healthCheck = &porterv1.HealthCheck{
  175. Enabled: service.HealthCheck.Enabled,
  176. HttpPath: service.HealthCheck.HttpPath,
  177. }
  178. }
  179. webConfig.HealthCheck = healthCheck
  180. domains := make([]*porterv1.Domain, 0)
  181. for _, domain := range service.Domains {
  182. domains = append(domains, &porterv1.Domain{
  183. Name: domain.Name,
  184. })
  185. }
  186. webConfig.Domains = domains
  187. serviceProto.Config = &porterv1.Service_WebConfig{
  188. WebConfig: webConfig,
  189. }
  190. case porterv1.ServiceType_SERVICE_TYPE_WORKER:
  191. workerConfig := &porterv1.WorkerServiceConfig{}
  192. var autoscaling *porterv1.Autoscaling
  193. if service.Autoscaling != nil {
  194. autoscaling = &porterv1.Autoscaling{
  195. Enabled: service.Autoscaling.Enabled,
  196. MinInstances: int32(service.Autoscaling.MinInstances),
  197. MaxInstances: int32(service.Autoscaling.MaxInstances),
  198. CpuThresholdPercent: int32(service.Autoscaling.CpuThresholdPercent),
  199. MemoryThresholdPercent: int32(service.Autoscaling.MemoryThresholdPercent),
  200. }
  201. }
  202. workerConfig.Autoscaling = autoscaling
  203. serviceProto.Config = &porterv1.Service_WorkerConfig{
  204. WorkerConfig: workerConfig,
  205. }
  206. case porterv1.ServiceType_SERVICE_TYPE_JOB:
  207. jobConfig := &porterv1.JobServiceConfig{
  208. AllowConcurrent: service.AllowConcurrent,
  209. Cron: service.Cron,
  210. }
  211. serviceProto.Config = &porterv1.Service_JobConfig{
  212. JobConfig: jobConfig,
  213. }
  214. }
  215. return serviceProto, nil
  216. }