yaml.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562
  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 := ProtoFromApp(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 := ProtoFromApp(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. // ServiceType is the type of a service in a Porter YAML file
  53. type ServiceType string
  54. const (
  55. // ServiceType_Web is type for web services specified in Porter YAML
  56. ServiceType_Web ServiceType = "web"
  57. // ServiceType_Worker is type for worker services specified in Porter YAML
  58. ServiceType_Worker ServiceType = "worker"
  59. // ServiceType_Job is type for job services specified in Porter YAML
  60. ServiceType_Job ServiceType = "job"
  61. )
  62. // EnvGroup is a struct containing the name and version of an environment group
  63. type EnvGroup struct {
  64. Name string `yaml:"name"`
  65. Version int `yaml:"version"`
  66. }
  67. // PorterApp represents all the possible fields in a Porter YAML file
  68. type PorterApp struct {
  69. Version string `yaml:"version,omitempty"`
  70. Name string `yaml:"name"`
  71. Services []Service `yaml:"services"`
  72. Image *Image `yaml:"image,omitempty"`
  73. Build *Build `yaml:"build,omitempty"`
  74. Env map[string]string `yaml:"env,omitempty"`
  75. Predeploy *Service `yaml:"predeploy,omitempty"`
  76. EnvGroups []EnvGroup `yaml:"envGroups,omitempty"`
  77. EfsStorage *EfsStorage `yaml:"efsStorage,omitempty"`
  78. RequiredApps []RequiredApp `yaml:"requiredApps,omitempty"`
  79. }
  80. // PorterYAML represents all the possible fields in a Porter YAML file
  81. type PorterYAML struct {
  82. PorterApp `yaml:",inline"`
  83. Previews *PorterApp `yaml:"previews,omitempty"`
  84. }
  85. // RequiredApp specifies another porter app that this app expects to be deployed alongside it
  86. type RequiredApp struct {
  87. Name string `yaml:"name"`
  88. FromTarget string `yaml:"fromTarget"`
  89. }
  90. // EfsStorage represents the EFS storage settings for a Porter app
  91. type EfsStorage struct {
  92. Enabled bool `yaml:"enabled"`
  93. }
  94. // Build represents the build settings for a Porter app
  95. type Build struct {
  96. Context string `yaml:"context" validate:"dir"`
  97. Method string `yaml:"method" validate:"required,oneof=pack docker registry"`
  98. Builder string `yaml:"builder" validate:"required_if=Method pack"`
  99. Buildpacks []string `yaml:"buildpacks"`
  100. Dockerfile string `yaml:"dockerfile" validate:"required_if=Method docker"`
  101. CommitSHA string `yaml:"commitSha"`
  102. }
  103. // Image is the repository and tag for an app's build image
  104. type Image struct {
  105. Repository string `yaml:"repository"`
  106. Tag string `yaml:"tag"`
  107. }
  108. // Service represents a single service in a porter app
  109. type Service struct {
  110. Name string `yaml:"name,omitempty"`
  111. Run *string `yaml:"run,omitempty"`
  112. Type ServiceType `yaml:"type,omitempty" validate:"required, oneof=web worker job"`
  113. Instances *int32 `yaml:"instances,omitempty"`
  114. CpuCores float32 `yaml:"cpuCores,omitempty"`
  115. RamMegabytes int `yaml:"ramMegabytes,omitempty"`
  116. GpuCoresNvidia float32 `yaml:"gpuCoresNvidia,omitempty"`
  117. SmartOptimization *bool `yaml:"smartOptimization,omitempty"`
  118. TerminationGracePeriodSeconds *int32 `yaml:"terminationGracePeriodSeconds,omitempty"`
  119. Port int `yaml:"port,omitempty"`
  120. Autoscaling *AutoScaling `yaml:"autoscaling,omitempty" validate:"excluded_if=Type job"`
  121. Domains []Domains `yaml:"domains,omitempty" validate:"excluded_unless=Type web"`
  122. HealthCheck *HealthCheck `yaml:"healthCheck,omitempty" validate:"excluded_unless=Type web"`
  123. AllowConcurrent *bool `yaml:"allowConcurrent,omitempty" validate:"excluded_unless=Type job"`
  124. Cron string `yaml:"cron,omitempty" validate:"excluded_unless=Type job"`
  125. SuspendCron *bool `yaml:"suspendCron,omitempty" validate:"excluded_unless=Type job"`
  126. TimeoutSeconds int `yaml:"timeoutSeconds,omitempty" validate:"excluded_unless=Type job"`
  127. Private *bool `yaml:"private,omitempty" validate:"excluded_unless=Type web"`
  128. IngressAnnotations map[string]string `yaml:"ingressAnnotations,omitempty" validate:"excluded_unless=Type web"`
  129. DisableTLS *bool `yaml:"disableTLS,omitempty" validate:"excluded_unless=Type web"`
  130. }
  131. // AutoScaling represents the autoscaling settings for web services
  132. type AutoScaling struct {
  133. Enabled bool `yaml:"enabled"`
  134. MinInstances int `yaml:"minInstances"`
  135. MaxInstances int `yaml:"maxInstances"`
  136. CpuThresholdPercent int `yaml:"cpuThresholdPercent"`
  137. MemoryThresholdPercent int `yaml:"memoryThresholdPercent"`
  138. }
  139. // Domains are the custom domains for a web service
  140. type Domains struct {
  141. Name string `yaml:"name"`
  142. }
  143. // HealthCheck is the health check settings for a web service
  144. type HealthCheck struct {
  145. Enabled bool `yaml:"enabled"`
  146. HttpPath string `yaml:"httpPath"`
  147. }
  148. // ProtoFromApp converts a PorterApp type to a base PorterApp proto type and returns env variables
  149. func ProtoFromApp(ctx context.Context, porterApp PorterApp) (*porterv1.PorterApp, map[string]string, error) {
  150. ctx, span := telemetry.NewSpan(ctx, "build-app-proto")
  151. defer span.End()
  152. appProto := &porterv1.PorterApp{
  153. Name: porterApp.Name,
  154. }
  155. if porterApp.Build != nil {
  156. appProto.Build = &porterv1.Build{
  157. Context: porterApp.Build.Context,
  158. Method: porterApp.Build.Method,
  159. Builder: porterApp.Build.Builder,
  160. Buildpacks: porterApp.Build.Buildpacks,
  161. Dockerfile: porterApp.Build.Dockerfile,
  162. CommitSha: porterApp.Build.CommitSHA,
  163. }
  164. }
  165. if porterApp.Image != nil {
  166. appProto.Image = &porterv1.AppImage{
  167. Repository: porterApp.Image.Repository,
  168. Tag: porterApp.Image.Tag,
  169. }
  170. }
  171. // service map is only needed for backwards compatibility at this time
  172. serviceMap := make(map[string]*porterv1.Service)
  173. var services []*porterv1.Service
  174. for _, service := range porterApp.Services {
  175. serviceType := protoEnumFromType(service.Name, service)
  176. serviceProto, err := serviceProtoFromConfig(service, serviceType)
  177. if err != nil {
  178. return appProto, nil, telemetry.Error(ctx, span, err, "error casting service config")
  179. }
  180. if service.Name == "" {
  181. return appProto, nil, telemetry.Error(ctx, span, nil, "service found with no name")
  182. }
  183. services = append(services, serviceProto)
  184. serviceMap[service.Name] = serviceProto
  185. }
  186. appProto.ServiceList = services
  187. appProto.Services = serviceMap // nolint:staticcheck // temporarily using deprecated field for backwards compatibility
  188. if porterApp.Predeploy != nil {
  189. predeployProto, err := serviceProtoFromConfig(*porterApp.Predeploy, porterv1.ServiceType_SERVICE_TYPE_JOB)
  190. if err != nil {
  191. return appProto, nil, telemetry.Error(ctx, span, err, "error casting predeploy config")
  192. }
  193. appProto.Predeploy = predeployProto
  194. }
  195. envGroups := make([]*porterv1.EnvGroup, 0)
  196. if porterApp.EnvGroups != nil {
  197. for _, envGroup := range porterApp.EnvGroups {
  198. envGroups = append(envGroups, &porterv1.EnvGroup{
  199. Name: envGroup.Name,
  200. Version: int64(envGroup.Version),
  201. })
  202. }
  203. }
  204. appProto.EnvGroups = envGroups
  205. if porterApp.EfsStorage != nil {
  206. appProto.EfsStorage = &porterv1.EFS{
  207. Enabled: porterApp.EfsStorage.Enabled,
  208. }
  209. }
  210. for _, requiredApp := range porterApp.RequiredApps {
  211. var targetIdentifier *porterv1.DeploymentTargetIdentifier
  212. if requiredApp.Name == "" {
  213. return appProto, nil, telemetry.Error(ctx, span, nil, "required app specified with no name")
  214. }
  215. if requiredApp.FromTarget != "" {
  216. targetIdentifier = &porterv1.DeploymentTargetIdentifier{
  217. Name: requiredApp.FromTarget,
  218. }
  219. }
  220. appProto.RequiredApps = append(appProto.RequiredApps, &porterv1.RequiredApp{
  221. Name: requiredApp.Name,
  222. FromTarget: targetIdentifier,
  223. })
  224. }
  225. return appProto, porterApp.Env, nil
  226. }
  227. func protoEnumFromType(name string, service Service) porterv1.ServiceType {
  228. serviceType := porterv1.ServiceType_SERVICE_TYPE_WORKER
  229. if strings.Contains(name, "web") {
  230. serviceType = porterv1.ServiceType_SERVICE_TYPE_WEB
  231. }
  232. if strings.Contains(name, "wkr") || strings.Contains(name, "worker") {
  233. serviceType = porterv1.ServiceType_SERVICE_TYPE_WORKER
  234. }
  235. if strings.Contains(name, "job") {
  236. serviceType = porterv1.ServiceType_SERVICE_TYPE_JOB
  237. }
  238. switch service.Type {
  239. case "web":
  240. serviceType = porterv1.ServiceType_SERVICE_TYPE_WEB
  241. case "worker":
  242. serviceType = porterv1.ServiceType_SERVICE_TYPE_WORKER
  243. case "job":
  244. serviceType = porterv1.ServiceType_SERVICE_TYPE_JOB
  245. }
  246. return serviceType
  247. }
  248. func serviceProtoFromConfig(service Service, serviceType porterv1.ServiceType) (*porterv1.Service, error) {
  249. serviceProto := &porterv1.Service{
  250. Name: service.Name,
  251. RunOptional: service.Run,
  252. InstancesOptional: service.Instances,
  253. CpuCores: service.CpuCores,
  254. RamMegabytes: int32(service.RamMegabytes),
  255. GpuCoresNvidia: service.GpuCoresNvidia,
  256. Port: int32(service.Port),
  257. SmartOptimization: service.SmartOptimization,
  258. Type: serviceType,
  259. TerminationGracePeriodSeconds: service.TerminationGracePeriodSeconds,
  260. }
  261. switch serviceType {
  262. default:
  263. return nil, fmt.Errorf("invalid service type '%s'", serviceType)
  264. case porterv1.ServiceType_SERVICE_TYPE_UNSPECIFIED:
  265. return nil, errors.New("Service type unspecified")
  266. case porterv1.ServiceType_SERVICE_TYPE_WEB:
  267. webConfig := &porterv1.WebServiceConfig{}
  268. var autoscaling *porterv1.Autoscaling
  269. if service.Autoscaling != nil {
  270. autoscaling = &porterv1.Autoscaling{
  271. Enabled: service.Autoscaling.Enabled,
  272. MinInstances: int32(service.Autoscaling.MinInstances),
  273. MaxInstances: int32(service.Autoscaling.MaxInstances),
  274. CpuThresholdPercent: int32(service.Autoscaling.CpuThresholdPercent),
  275. MemoryThresholdPercent: int32(service.Autoscaling.MemoryThresholdPercent),
  276. }
  277. }
  278. webConfig.Autoscaling = autoscaling
  279. var healthCheck *porterv1.HealthCheck
  280. if service.HealthCheck != nil {
  281. healthCheck = &porterv1.HealthCheck{
  282. Enabled: service.HealthCheck.Enabled,
  283. HttpPath: service.HealthCheck.HttpPath,
  284. }
  285. }
  286. webConfig.HealthCheck = healthCheck
  287. domains := make([]*porterv1.Domain, 0)
  288. for _, domain := range service.Domains {
  289. domains = append(domains, &porterv1.Domain{
  290. Name: domain.Name,
  291. })
  292. }
  293. webConfig.Domains = domains
  294. webConfig.IngressAnnotations = service.IngressAnnotations
  295. if service.Private != nil {
  296. webConfig.Private = service.Private
  297. }
  298. if service.DisableTLS != nil {
  299. webConfig.DisableTls = service.DisableTLS
  300. }
  301. serviceProto.Config = &porterv1.Service_WebConfig{
  302. WebConfig: webConfig,
  303. }
  304. case porterv1.ServiceType_SERVICE_TYPE_WORKER:
  305. workerConfig := &porterv1.WorkerServiceConfig{}
  306. var autoscaling *porterv1.Autoscaling
  307. if service.Autoscaling != nil {
  308. autoscaling = &porterv1.Autoscaling{
  309. Enabled: service.Autoscaling.Enabled,
  310. MinInstances: int32(service.Autoscaling.MinInstances),
  311. MaxInstances: int32(service.Autoscaling.MaxInstances),
  312. CpuThresholdPercent: int32(service.Autoscaling.CpuThresholdPercent),
  313. MemoryThresholdPercent: int32(service.Autoscaling.MemoryThresholdPercent),
  314. }
  315. }
  316. workerConfig.Autoscaling = autoscaling
  317. serviceProto.Config = &porterv1.Service_WorkerConfig{
  318. WorkerConfig: workerConfig,
  319. }
  320. case porterv1.ServiceType_SERVICE_TYPE_JOB:
  321. jobConfig := &porterv1.JobServiceConfig{
  322. AllowConcurrentOptional: service.AllowConcurrent,
  323. Cron: service.Cron,
  324. }
  325. if service.SuspendCron != nil {
  326. jobConfig.SuspendCron = service.SuspendCron
  327. }
  328. if service.TimeoutSeconds != 0 {
  329. jobConfig.TimeoutSeconds = int64(service.TimeoutSeconds)
  330. }
  331. serviceProto.Config = &porterv1.Service_JobConfig{
  332. JobConfig: jobConfig,
  333. }
  334. }
  335. return serviceProto, nil
  336. }
  337. // AppFromProto converts a PorterApp proto object into a PorterApp struct
  338. func AppFromProto(appProto *porterv1.PorterApp) (PorterApp, error) {
  339. porterApp := PorterApp{
  340. Version: "v2",
  341. Name: appProto.Name,
  342. }
  343. if appProto.Build != nil {
  344. porterApp.Build = &Build{
  345. Context: appProto.Build.Context,
  346. Method: appProto.Build.Method,
  347. Builder: appProto.Build.Builder,
  348. Buildpacks: appProto.Build.Buildpacks,
  349. Dockerfile: appProto.Build.Dockerfile,
  350. CommitSHA: appProto.Build.CommitSha,
  351. }
  352. }
  353. if appProto.Image != nil {
  354. porterApp.Image = &Image{
  355. Repository: appProto.Image.Repository,
  356. Tag: appProto.Image.Tag,
  357. }
  358. }
  359. uniqueServices := uniqueServices(appProto.Services, appProto.ServiceList) // nolint:staticcheck // temporarily using deprecated field for backwards compatibility
  360. for _, service := range uniqueServices {
  361. appService, err := appServiceFromProto(service)
  362. if err != nil {
  363. return porterApp, err
  364. }
  365. porterApp.Services = append(porterApp.Services, appService)
  366. }
  367. if appProto.Predeploy != nil {
  368. appPredeploy, err := appServiceFromProto(appProto.Predeploy)
  369. if err != nil {
  370. return porterApp, err
  371. }
  372. porterApp.Predeploy = &appPredeploy
  373. }
  374. porterApp.EnvGroups = make([]EnvGroup, 0)
  375. for _, envGroup := range appProto.EnvGroups {
  376. porterApp.EnvGroups = append(porterApp.EnvGroups, EnvGroup{
  377. Name: envGroup.Name,
  378. Version: int(envGroup.Version),
  379. })
  380. }
  381. if appProto.EfsStorage != nil {
  382. porterApp.EfsStorage = &EfsStorage{
  383. Enabled: appProto.EfsStorage.Enabled,
  384. }
  385. }
  386. return porterApp, nil
  387. }
  388. func appServiceFromProto(service *porterv1.Service) (Service, error) {
  389. appService := Service{
  390. Name: service.Name,
  391. Run: service.RunOptional,
  392. Instances: service.InstancesOptional,
  393. CpuCores: service.CpuCores,
  394. RamMegabytes: int(service.RamMegabytes),
  395. GpuCoresNvidia: service.GpuCoresNvidia, // nolint:staticcheck // https://linear.app/porter/issue/POR-2137/support-new-gpu-field-in-porteryaml
  396. Port: int(service.Port),
  397. SmartOptimization: service.SmartOptimization,
  398. TerminationGracePeriodSeconds: service.TerminationGracePeriodSeconds,
  399. }
  400. switch service.Type {
  401. default:
  402. return appService, fmt.Errorf("invalid service type '%s'", service.Type)
  403. case porterv1.ServiceType_SERVICE_TYPE_UNSPECIFIED:
  404. return appService, errors.New("Service type unspecified")
  405. case porterv1.ServiceType_SERVICE_TYPE_WEB:
  406. webConfig := service.GetWebConfig()
  407. appService.Type = "web"
  408. var autoscaling *AutoScaling
  409. if webConfig.Autoscaling != nil {
  410. autoscaling = &AutoScaling{
  411. Enabled: webConfig.Autoscaling.Enabled,
  412. MinInstances: int(webConfig.Autoscaling.MinInstances),
  413. MaxInstances: int(webConfig.Autoscaling.MaxInstances),
  414. CpuThresholdPercent: int(webConfig.Autoscaling.CpuThresholdPercent),
  415. MemoryThresholdPercent: int(webConfig.Autoscaling.MemoryThresholdPercent),
  416. }
  417. }
  418. appService.Autoscaling = autoscaling
  419. var healthCheck *HealthCheck
  420. if webConfig.HealthCheck != nil {
  421. healthCheck = &HealthCheck{
  422. Enabled: webConfig.HealthCheck.Enabled,
  423. HttpPath: webConfig.HealthCheck.HttpPath,
  424. }
  425. }
  426. appService.HealthCheck = healthCheck
  427. domains := make([]Domains, 0)
  428. for _, domain := range webConfig.Domains {
  429. domains = append(domains, Domains{
  430. Name: domain.Name,
  431. })
  432. }
  433. appService.Domains = domains
  434. appService.IngressAnnotations = webConfig.IngressAnnotations
  435. if webConfig.Private != nil {
  436. appService.Private = webConfig.Private
  437. }
  438. if webConfig.DisableTls != nil {
  439. appService.DisableTLS = webConfig.DisableTls
  440. }
  441. case porterv1.ServiceType_SERVICE_TYPE_WORKER:
  442. workerConfig := service.GetWorkerConfig()
  443. appService.Type = "worker"
  444. var autoscaling *AutoScaling
  445. if workerConfig.Autoscaling != nil {
  446. autoscaling = &AutoScaling{
  447. Enabled: workerConfig.Autoscaling.Enabled,
  448. MinInstances: int(workerConfig.Autoscaling.MinInstances),
  449. MaxInstances: int(workerConfig.Autoscaling.MaxInstances),
  450. CpuThresholdPercent: int(workerConfig.Autoscaling.CpuThresholdPercent),
  451. MemoryThresholdPercent: int(workerConfig.Autoscaling.MemoryThresholdPercent),
  452. }
  453. }
  454. appService.Autoscaling = autoscaling
  455. case porterv1.ServiceType_SERVICE_TYPE_JOB:
  456. jobConfig := service.GetJobConfig()
  457. appService.Type = "job"
  458. appService.AllowConcurrent = jobConfig.AllowConcurrentOptional
  459. appService.Cron = jobConfig.Cron
  460. appService.SuspendCron = jobConfig.SuspendCron
  461. appService.TimeoutSeconds = int(jobConfig.TimeoutSeconds)
  462. }
  463. return appService, nil
  464. }
  465. func uniqueServices(serviceMap map[string]*porterv1.Service, serviceList []*porterv1.Service) []*porterv1.Service {
  466. if serviceList != nil {
  467. return serviceList
  468. }
  469. // deduplicate services by name, favoring whatever was defined first
  470. uniqueServices := make(map[string]*porterv1.Service)
  471. for name, service := range serviceMap {
  472. service.Name = name
  473. uniqueServices[service.Name] = service
  474. }
  475. mergedServiceList := make([]*porterv1.Service, 0)
  476. for _, service := range uniqueServices {
  477. mergedServiceList = append(mergedServiceList, service)
  478. }
  479. return mergedServiceList
  480. }