yaml.go 18 KB

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