yaml.go 9.7 KB

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