parse.go 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. package porter_app
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "github.com/porter-dev/porter/api/server/shared/config"
  8. "github.com/porter-dev/porter/api/types"
  9. porterAppUtils "github.com/porter-dev/porter/api/utils/porter_app"
  10. "github.com/porter-dev/porter/internal/helm/loader"
  11. "github.com/porter-dev/porter/internal/integrations/dns"
  12. "github.com/porter-dev/porter/internal/kubernetes"
  13. "github.com/porter-dev/porter/internal/kubernetes/domain"
  14. "github.com/porter-dev/porter/internal/kubernetes/environment_groups"
  15. "github.com/porter-dev/porter/internal/kubernetes/porter_app"
  16. "github.com/porter-dev/porter/internal/repository"
  17. "github.com/porter-dev/porter/internal/telemetry"
  18. "github.com/porter-dev/porter/internal/templater/utils"
  19. "github.com/stefanmcshane/helm/pkg/chart"
  20. "gopkg.in/yaml.v2"
  21. )
  22. type PorterStackYAML struct {
  23. Applications map[string]*Application `yaml:"applications" validate:"required_without=Services Apps"`
  24. Version *string `yaml:"version"`
  25. Build *Build `yaml:"build"`
  26. Env map[string]string `yaml:"env"`
  27. SyncedEnv []*SyncedEnvSection `yaml:"synced_env"`
  28. Apps map[string]*Service `yaml:"apps" validate:"required_without=Applications Services"`
  29. Services map[string]*Service `yaml:"services" validate:"required_without=Applications Apps"`
  30. Release *Service `yaml:"release"`
  31. }
  32. type Application struct {
  33. Services map[string]*Service `yaml:"services" validate:"required"`
  34. Build *Build `yaml:"build"`
  35. Env map[string]string `yaml:"env"`
  36. Release *Service `yaml:"release"`
  37. }
  38. type Build struct {
  39. Context *string `yaml:"context" validate:"dir"`
  40. Method *string `yaml:"method" validate:"required,oneof=pack docker registry"`
  41. Builder *string `yaml:"builder" validate:"required_if=Method pack"`
  42. Buildpacks []*string `yaml:"buildpacks"`
  43. Dockerfile *string `yaml:"dockerfile" validate:"required_if=Method docker"`
  44. Image *string `yaml:"image" validate:"required_if=Method registry"`
  45. }
  46. type Service struct {
  47. Run *string `yaml:"run"`
  48. Config map[string]interface{} `yaml:"config"`
  49. Type *string `yaml:"type" validate:"required, oneof=web worker job"`
  50. }
  51. type SyncedEnvSection struct {
  52. Name string `json:"name" yaml:"name"`
  53. Version uint `json:"version" yaml:"version"`
  54. Keys []SyncedEnvSectionKey `json:"keys" yaml:"keys"`
  55. }
  56. type SyncedEnvSectionKey struct {
  57. Name string `json:"name" yaml:"name"`
  58. Secret bool `json:"secret" yaml:"secret"`
  59. }
  60. type SubdomainCreateOpts struct {
  61. k8sAgent *kubernetes.Agent
  62. dnsRepo repository.DNSRecordRepository
  63. dnsClient *dns.Client
  64. appRootDomain string
  65. stackName string
  66. }
  67. type ParseConf struct {
  68. // PorterAppName is the name of the porter app
  69. PorterAppName string
  70. // PorterYaml is the raw porter yaml which is used to build the values + chart for helm upgrade
  71. PorterYaml []byte
  72. // ImageInfo contains the repository and tag of the image to use for the helm upgrade. Kept separate from the PorterYaml because the image info
  73. // is stored in the 'global' key of the values, which is not part of the porter yaml
  74. ImageInfo types.ImageInfo
  75. // ServerConfig is the server conf, used to find the default helm repo
  76. ServerConfig *config.Config
  77. // ProjectID
  78. ProjectID uint
  79. // UserUpdate used for synced env groups
  80. UserUpdate bool
  81. // EnvGroups used for synced env groups
  82. EnvGroups []string
  83. // EnvironmentGroups are used for syncing environment groups using ConfigMaps and Secrets from porter-env-groups namespace. This should be used instead of EnvGroups
  84. EnvironmentGroups []string
  85. // Namespace used for synced env groups
  86. Namespace string
  87. // ExistingHelmValues is the existing values for the helm release, if it exists
  88. ExistingHelmValues map[string]interface{}
  89. // ExistingChartDependencies is the existing dependencies for the helm release, if it exists
  90. ExistingChartDependencies []*chart.Dependency
  91. // SubdomainCreateOpts contains the necessary information to create a subdomain if necessary
  92. SubdomainCreateOpts SubdomainCreateOpts
  93. // InjectLauncherToStartCommand is a flag to determine whether to prepend the launcher to the start command
  94. InjectLauncherToStartCommand bool
  95. // ShouldValidateHelmValues is a flag to determine whether to validate helm values
  96. ShouldValidateHelmValues bool
  97. // FullHelmValues if provided, override anything specified in porter.yaml. Used as an escape hatch for support
  98. FullHelmValues string
  99. // AddCustomNodeSelector is a flag to determine whether to add porter.run/workload-kind: application to the nodeselector attribute of the helm values
  100. AddCustomNodeSelector bool
  101. // RemoveDeletedServices is a flag to determine whether to remove values and dependencies for services that are not defined in the porter.yaml
  102. RemoveDeletedServices bool
  103. }
  104. func parse(ctx context.Context, conf ParseConf) (*chart.Chart, map[string]interface{}, map[string]interface{}, error) {
  105. ctx, span := telemetry.NewSpan(ctx, "parse-porter-yaml")
  106. defer span.End()
  107. parsed := &PorterStackYAML{}
  108. if conf.FullHelmValues != "" {
  109. parsedHelmValues, err := convertHelmValuesToPorterYaml(conf.FullHelmValues)
  110. if err != nil {
  111. err = telemetry.Error(ctx, span, err, "error parsing raw helm values")
  112. return nil, nil, nil, err
  113. }
  114. parsed = parsedHelmValues
  115. } else {
  116. err := yaml.Unmarshal(conf.PorterYaml, parsed)
  117. if err != nil {
  118. err = telemetry.Error(ctx, span, err, "error parsing porter.yaml")
  119. return nil, nil, nil, err
  120. }
  121. }
  122. synced_env := make([]*SyncedEnvSection, 0)
  123. for i := range conf.EnvGroups {
  124. cm, _, err := conf.SubdomainCreateOpts.k8sAgent.GetLatestVersionedConfigMap(conf.EnvGroups[i], conf.Namespace)
  125. if err != nil {
  126. err = telemetry.Error(ctx, span, err, "error getting latest versioned config map")
  127. return nil, nil, nil, err
  128. }
  129. versionStr, ok := cm.ObjectMeta.Labels["version"]
  130. if !ok {
  131. err = telemetry.Error(ctx, span, nil, "error extracting version from config map")
  132. return nil, nil, nil, err
  133. }
  134. versionInt, err := strconv.Atoi(versionStr)
  135. if err != nil {
  136. err = telemetry.Error(ctx, span, err, "error converting version to int")
  137. return nil, nil, nil, err
  138. }
  139. version := uint(versionInt)
  140. newSection := &SyncedEnvSection{
  141. Name: conf.EnvGroups[i],
  142. Version: version,
  143. }
  144. newSectionKeys := make([]SyncedEnvSectionKey, 0)
  145. for key, val := range cm.Data {
  146. newSectionKeys = append(newSectionKeys, SyncedEnvSectionKey{
  147. Name: key,
  148. Secret: strings.Contains(val, "PORTERSECRET"),
  149. })
  150. }
  151. newSection.Keys = newSectionKeys
  152. synced_env = append(synced_env, newSection)
  153. }
  154. if parsed.Apps != nil && parsed.Services != nil {
  155. err := telemetry.Error(ctx, span, nil, "'apps' and 'services' are synonymous but both were defined")
  156. return nil, nil, nil, err
  157. }
  158. var services map[string]*Service
  159. if parsed.Apps != nil {
  160. services = parsed.Apps
  161. }
  162. if parsed.Services != nil {
  163. services = parsed.Services
  164. }
  165. for serviceName := range services {
  166. services[serviceName] = addLabelsToService(services[serviceName], conf.EnvironmentGroups, porter_app.LabelKey_PorterApplication)
  167. }
  168. application := &Application{
  169. Env: parsed.Env,
  170. Services: services,
  171. Build: parsed.Build,
  172. Release: parsed.Release,
  173. }
  174. values, err := buildUmbrellaChartValues(ctx, application, synced_env, conf.ImageInfo, conf.ExistingHelmValues, conf.SubdomainCreateOpts, conf.InjectLauncherToStartCommand, conf.ShouldValidateHelmValues, conf.UserUpdate, conf.Namespace, conf.AddCustomNodeSelector, conf.RemoveDeletedServices)
  175. if err != nil {
  176. err = telemetry.Error(ctx, span, err, "error building values")
  177. return nil, nil, nil, err
  178. }
  179. convertedValues, ok := convertMap(values).(map[string]interface{})
  180. if !ok {
  181. err = telemetry.Error(ctx, span, nil, "error converting values")
  182. return nil, nil, nil, err
  183. }
  184. umbrellaChart, err := buildUmbrellaChart(application, conf.ServerConfig, conf.ProjectID, conf.ExistingChartDependencies, conf.RemoveDeletedServices)
  185. if err != nil {
  186. err = telemetry.Error(ctx, span, err, "error building umbrella chart")
  187. return nil, nil, nil, err
  188. }
  189. // return the parsed release values for the release job chart, if they exist
  190. var preDeployJobValues map[string]interface{}
  191. if application.Release != nil && application.Release.Run != nil {
  192. application.Release = addLabelsToService(application.Release, conf.EnvironmentGroups, porter_app.LabelKey_PorterApplicationPreDeploy)
  193. preDeployJobValues = buildPreDeployJobChartValues(application.Release, application.Env, synced_env, conf.ImageInfo, conf.InjectLauncherToStartCommand, conf.ExistingHelmValues, porterAppUtils.PredeployJobNameFromPorterAppName(conf.PorterAppName), conf.UserUpdate, conf.AddCustomNodeSelector)
  194. }
  195. return umbrellaChart, convertedValues, preDeployJobValues, nil
  196. }
  197. func buildUmbrellaChartValues(
  198. ctx context.Context,
  199. application *Application,
  200. syncedEnv []*SyncedEnvSection,
  201. imageInfo types.ImageInfo,
  202. existingValues map[string]interface{},
  203. opts SubdomainCreateOpts,
  204. injectLauncher bool,
  205. shouldValidateHelmValues bool,
  206. userUpdate bool,
  207. namespace string,
  208. addCustomNodeSelector bool,
  209. removeDeletedValues bool,
  210. ) (map[string]interface{}, error) {
  211. values := make(map[string]interface{})
  212. if application.Services == nil {
  213. if existingValues == nil {
  214. return nil, fmt.Errorf("porter.yaml must contain at least one service, or pre-deploy must exist and have values")
  215. }
  216. }
  217. for name, service := range application.Services {
  218. serviceType := getType(name, service)
  219. defaultValues := getDefaultValues(service, application.Env, syncedEnv, serviceType, existingValues, name, userUpdate, addCustomNodeSelector)
  220. convertedConfig := convertMap(service.Config).(map[string]interface{})
  221. helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
  222. // required to identify the chart type because of https://github.com/helm/helm/issues/9214
  223. helmName := getHelmName(name, serviceType)
  224. if existingValues != nil {
  225. if existingValues[helmName] != nil {
  226. existingValuesMap := existingValues[helmName].(map[string]interface{})
  227. helm_values = utils.DeepCoalesceValues(existingValuesMap, helm_values)
  228. }
  229. }
  230. validateErr := validateHelmValues(helm_values, shouldValidateHelmValues, serviceType)
  231. if validateErr != "" {
  232. return nil, fmt.Errorf("error validating service \"%s\": %s", name, validateErr)
  233. }
  234. err := syncEnvironmentGroupToNamespaceIfLabelsExist(ctx, opts.k8sAgent, service, namespace)
  235. if err != nil {
  236. return nil, fmt.Errorf("error syncing environment group to namespace: %w", err)
  237. }
  238. err = createSubdomainIfRequired(helm_values, opts) // modifies helm_values to add subdomains if necessary
  239. if err != nil {
  240. return nil, err
  241. }
  242. // just in case this slips by
  243. if serviceType == "web" {
  244. if helm_values["ingress"] == nil {
  245. helm_values["ingress"] = map[string]interface{}{
  246. "enabled": false,
  247. }
  248. }
  249. }
  250. values[helmName] = helm_values
  251. }
  252. if !removeDeletedValues {
  253. // add back in the existing services that were not overwritten
  254. for k, v := range existingValues {
  255. if values[k] == nil {
  256. values[k] = v
  257. }
  258. }
  259. }
  260. // prepend launcher to all start commands if we need to
  261. for _, v := range values {
  262. if serviceValues, ok := v.(map[string]interface{}); ok {
  263. if serviceValues["container"] != nil {
  264. containerMap := serviceValues["container"].(map[string]interface{})
  265. if containerMap["command"] != nil {
  266. command := containerMap["command"].(string)
  267. if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
  268. containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
  269. }
  270. }
  271. }
  272. }
  273. }
  274. values["global"] = map[string]interface{}{
  275. "image": map[string]interface{}{
  276. "repository": imageInfo.Repository,
  277. "tag": imageInfo.Tag,
  278. },
  279. }
  280. return values, nil
  281. }
  282. // syncEnvironmentGroupToNamespaceIfLabelsExist will sync the latest version of the environment group to the target namespace if the service has the appropriate label.
  283. func syncEnvironmentGroupToNamespaceIfLabelsExist(ctx context.Context, agent *kubernetes.Agent, service *Service, targetNamespace string) error {
  284. var linkedGroupNames string
  285. // patchwork because we are not consistent with the type of labels
  286. if labels, ok := service.Config["labels"].(map[string]any); ok {
  287. if linkedGroup, ok := labels[environment_groups.LabelKey_LinkedEnvironmentGroup].(string); ok {
  288. linkedGroupNames = linkedGroup
  289. }
  290. }
  291. if labels, ok := service.Config["labels"].(map[string]string); ok {
  292. if linkedGroup, ok := labels[environment_groups.LabelKey_LinkedEnvironmentGroup]; ok {
  293. linkedGroupNames = linkedGroup
  294. }
  295. }
  296. for _, linkedGroupName := range strings.Split(linkedGroupNames, ".") {
  297. inp := environment_groups.SyncLatestVersionToNamespaceInput{
  298. BaseEnvironmentGroupName: linkedGroupName,
  299. TargetNamespace: targetNamespace,
  300. }
  301. syncedEnvironment, err := environment_groups.SyncLatestVersionToNamespace(ctx, agent, inp)
  302. if err != nil {
  303. return fmt.Errorf("error syncing environment group: %w", err)
  304. }
  305. if syncedEnvironment.EnvironmentGroupVersionedName != "" {
  306. if service.Config["configMapRefs"] == nil {
  307. service.Config["configMapRefs"] = []string{}
  308. }
  309. if service.Config["secretRefs"] == nil {
  310. service.Config["secretRefs"] = []string{}
  311. }
  312. switch service.Config["configMapRefs"].(type) {
  313. case []string:
  314. service.Config["configMapRefs"] = append(service.Config["configMapRefs"].([]string), syncedEnvironment.EnvironmentGroupVersionedName)
  315. case []any:
  316. service.Config["configMapRefs"] = append(service.Config["configMapRefs"].([]any), syncedEnvironment.EnvironmentGroupVersionedName)
  317. }
  318. switch service.Config["configMapRefs"].(type) {
  319. case []string:
  320. service.Config["secretRefs"] = append(service.Config["secretRefs"].([]string), syncedEnvironment.EnvironmentGroupVersionedName)
  321. case []any:
  322. service.Config["secretRefs"] = append(service.Config["secretRefs"].([]any), syncedEnvironment.EnvironmentGroupVersionedName)
  323. }
  324. }
  325. }
  326. return nil
  327. }
  328. // we can add to this function up later or use an alternative
  329. func validateHelmValues(values map[string]interface{}, shouldValidateHelmValues bool, appType string) string {
  330. if shouldValidateHelmValues {
  331. // validate port for web services
  332. if appType == "web" {
  333. containerMap, err := getNestedMap(values, "container")
  334. if err != nil {
  335. return "error checking port: misformatted values"
  336. } else {
  337. portVal, portExists := containerMap["port"]
  338. if portExists {
  339. portStr, pOK := portVal.(string)
  340. if !pOK {
  341. return "error checking port: no port in container"
  342. }
  343. port, err := strconv.Atoi(portStr)
  344. if err != nil || port < 1024 || port > 65535 {
  345. return "port must be a number between 1024 and 65535"
  346. }
  347. } else {
  348. return "port must be specified for web services"
  349. }
  350. }
  351. }
  352. }
  353. return ""
  354. }
  355. func buildPreDeployJobChartValues(release *Service, env map[string]string, synced_env []*SyncedEnvSection, imageInfo types.ImageInfo, injectLauncher bool, existingValues map[string]interface{}, name string, userUpdate bool, addCustomNodeSelector bool) map[string]interface{} {
  356. defaultValues := getDefaultValues(release, env, synced_env, "job", existingValues, name+"-r", userUpdate, addCustomNodeSelector)
  357. convertedConfig := convertMap(release.Config).(map[string]interface{})
  358. helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
  359. if imageInfo.Repository != "" && imageInfo.Tag != "" {
  360. helm_values["image"] = map[string]interface{}{
  361. "repository": imageInfo.Repository,
  362. "tag": imageInfo.Tag,
  363. }
  364. }
  365. // prepend launcher if we need to
  366. if helm_values["container"] != nil {
  367. containerMap := helm_values["container"].(map[string]interface{})
  368. if containerMap["command"] != nil {
  369. command := containerMap["command"].(string)
  370. if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
  371. containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
  372. }
  373. }
  374. }
  375. return helm_values
  376. }
  377. func getType(name string, service *Service) string {
  378. if service.Type != nil {
  379. return *service.Type
  380. }
  381. if strings.Contains(name, "web") {
  382. return "web"
  383. }
  384. if strings.Contains(name, "job") {
  385. return "job"
  386. }
  387. return "worker"
  388. }
  389. func getDefaultValues(service *Service, env map[string]string, synced_env []*SyncedEnvSection, appType string, existingValues map[string]interface{}, name string, userUpdate bool, addCustomNodeSelector bool) map[string]interface{} {
  390. var defaultValues map[string]interface{}
  391. var runCommand string
  392. if service.Run != nil {
  393. runCommand = *service.Run
  394. }
  395. var syncedEnvs []map[string]interface{}
  396. envConf, err := getStacksNestedMap(existingValues, name+"-"+appType, "container", "env")
  397. if !userUpdate && err == nil {
  398. syncedEnvs = envConf
  399. } else {
  400. syncedEnvs = deconstructSyncedEnvs(synced_env, env)
  401. }
  402. defaultValues = map[string]interface{}{
  403. "container": map[string]interface{}{
  404. "command": runCommand,
  405. "env": map[string]interface{}{
  406. "normal": CopyEnv(env),
  407. "synced": syncedEnvs,
  408. },
  409. },
  410. "nodeSelector": map[string]interface{}{},
  411. }
  412. if addCustomNodeSelector {
  413. defaultValues["nodeSelector"] = map[string]interface{}{
  414. "porter.run/workload-kind": "application",
  415. }
  416. }
  417. return defaultValues
  418. }
  419. func deconstructSyncedEnvs(synced_env []*SyncedEnvSection, env map[string]string) []map[string]interface{} {
  420. synced := make([]map[string]interface{}, 0)
  421. for _, group := range synced_env {
  422. keys := make([]map[string]interface{}, 0)
  423. for _, key := range group.Keys {
  424. if _, exists := env[key.Name]; !exists {
  425. // Only include keys not present in env
  426. keys = append(keys, map[string]interface{}{
  427. "name": key.Name,
  428. "secret": key.Secret,
  429. })
  430. }
  431. }
  432. syncedGroup := map[string]interface{}{
  433. "keys": keys,
  434. "name": group.Name,
  435. "version": group.Version,
  436. }
  437. synced = append(synced, syncedGroup)
  438. }
  439. return synced
  440. }
  441. func buildUmbrellaChart(application *Application, config *config.Config, projectID uint, existingDependencies []*chart.Dependency, removeDeletedDependencies bool) (*chart.Chart, error) {
  442. deps := make([]*chart.Dependency, 0)
  443. for alias, service := range application.Services {
  444. var serviceType string
  445. if existingDependencies != nil {
  446. for _, dep := range existingDependencies {
  447. // this condition checks that the dependency is of the form <alias>-web or <alias>-wkr or <alias>-job, meaning it already exists in the chart
  448. if strings.HasPrefix(dep.Alias, fmt.Sprintf("%s-", alias)) && (strings.HasSuffix(dep.Alias, "-web") || strings.HasSuffix(dep.Alias, "-wkr") || strings.HasSuffix(dep.Alias, "-job")) {
  449. serviceType = getChartTypeFromHelmName(dep.Alias)
  450. if serviceType == "" {
  451. return nil, fmt.Errorf("unable to determine type of existing dependency")
  452. }
  453. }
  454. }
  455. // this is a new app, so we need to get the type from the app name or type
  456. if serviceType == "" {
  457. serviceType = getType(alias, service)
  458. }
  459. } else {
  460. serviceType = getType(alias, service)
  461. }
  462. selectedRepo := config.ServerConf.DefaultApplicationHelmRepoURL
  463. selectedVersion, err := getLatestTemplateVersion(serviceType, config, projectID)
  464. if err != nil {
  465. return nil, err
  466. }
  467. helmName := getHelmName(alias, serviceType)
  468. deps = append(deps, &chart.Dependency{
  469. Name: serviceType,
  470. Alias: helmName,
  471. Version: selectedVersion,
  472. Repository: selectedRepo,
  473. })
  474. }
  475. if !removeDeletedDependencies {
  476. // add in the existing dependencies that were not overwritten
  477. for _, dep := range existingDependencies {
  478. if !dependencyExists(deps, dep) {
  479. // have to repair the dependency name because of https://github.com/helm/helm/issues/9214
  480. if strings.HasSuffix(dep.Name, "-web") || strings.HasSuffix(dep.Name, "-wkr") || strings.HasSuffix(dep.Name, "-job") {
  481. dep.Name = getChartTypeFromHelmName(dep.Name)
  482. if dep.Name == "" {
  483. return nil, fmt.Errorf("unable to determine type of existing dependency")
  484. }
  485. version, err := getLatestTemplateVersion(dep.Name, config, projectID)
  486. if err != nil {
  487. return nil, err
  488. }
  489. dep.Version = version
  490. }
  491. deps = append(deps, dep)
  492. }
  493. }
  494. }
  495. chart, err := createChartFromDependencies(deps)
  496. if err != nil {
  497. return nil, err
  498. }
  499. return chart, nil
  500. }
  501. func dependencyExists(deps []*chart.Dependency, dep *chart.Dependency) bool {
  502. for _, d := range deps {
  503. if d.Alias == dep.Alias {
  504. return true
  505. }
  506. }
  507. return false
  508. }
  509. func createChartFromDependencies(deps []*chart.Dependency) (*chart.Chart, error) {
  510. metadata := &chart.Metadata{
  511. Name: "umbrella",
  512. Description: "Web application that is exposed to external traffic.",
  513. Version: "0.96.0",
  514. APIVersion: "v2",
  515. Home: "https://getporter.dev/",
  516. Icon: "https://user-images.githubusercontent.com/65516095/111255214-07d3da80-85ed-11eb-99e2-fddcbdb99bdb.png",
  517. Keywords: []string{
  518. "porter",
  519. "application",
  520. "service",
  521. "umbrella",
  522. },
  523. Type: "application",
  524. Dependencies: deps,
  525. }
  526. // create a new chart object with the metadata
  527. c := &chart.Chart{
  528. Metadata: metadata,
  529. }
  530. return c, nil
  531. }
  532. func getLatestTemplateVersion(templateName string, config *config.Config, projectID uint) (string, error) {
  533. repoIndex, err := loader.LoadRepoIndexPublic(config.ServerConf.DefaultApplicationHelmRepoURL)
  534. if err != nil {
  535. return "", fmt.Errorf("%s: %w", "unable to load porter chart repo", err)
  536. }
  537. templates := loader.RepoIndexToPorterChartList(repoIndex, config.ServerConf.DefaultApplicationHelmRepoURL)
  538. if err != nil {
  539. return "", fmt.Errorf("%s: %w", "unable to load porter chart list", err)
  540. }
  541. var version string
  542. // find the matching template name
  543. for _, template := range templates {
  544. if templateName == template.Name {
  545. version = template.Versions[0]
  546. break
  547. }
  548. }
  549. if version == "" {
  550. return "", fmt.Errorf("matching template version not found")
  551. }
  552. return version, nil
  553. }
  554. func convertMap(m interface{}) interface{} {
  555. switch m := m.(type) {
  556. case map[string]interface{}:
  557. for k, v := range m {
  558. m[k] = convertMap(v)
  559. }
  560. case map[string]string:
  561. result := map[string]interface{}{}
  562. for k, v := range m {
  563. result[k] = v
  564. }
  565. return result
  566. case map[interface{}]interface{}:
  567. result := map[string]interface{}{}
  568. for k, v := range m {
  569. result[k.(string)] = convertMap(v)
  570. }
  571. return result
  572. case []interface{}:
  573. for i, v := range m {
  574. m[i] = convertMap(v)
  575. }
  576. }
  577. return m
  578. }
  579. func CopyEnv(env map[string]string) map[string]interface{} {
  580. envCopy := make(map[string]interface{})
  581. if env == nil {
  582. return envCopy
  583. }
  584. for k, v := range env {
  585. if k == "" || v == "" {
  586. continue
  587. }
  588. envCopy[k] = v
  589. }
  590. return envCopy
  591. }
  592. func createSubdomainIfRequired(
  593. mergedValues map[string]interface{},
  594. opts SubdomainCreateOpts,
  595. ) error {
  596. // look for ingress.enabled and no custom domains set
  597. ingressMap, err := getNestedMap(mergedValues, "ingress")
  598. if err == nil {
  599. enabledVal, enabledExists := ingressMap["enabled"]
  600. if enabledExists {
  601. enabled, eOK := enabledVal.(bool)
  602. if eOK && enabled {
  603. // if custom domain, we don't need to create a subdomain
  604. customDomVal, customDomExists := ingressMap["custom_domain"]
  605. if customDomExists {
  606. customDomain, cOK := customDomVal.(bool)
  607. if cOK && customDomain {
  608. return nil
  609. }
  610. }
  611. // subdomain already exists, no need to create one
  612. if porterHosts, ok := ingressMap["porter_hosts"].([]interface{}); ok && len(porterHosts) > 0 {
  613. return nil
  614. }
  615. // in the case of ingress enabled but no custom domain, create subdomain
  616. dnsRecord, err := createDNSRecord(opts)
  617. if err != nil {
  618. return fmt.Errorf("error creating subdomain: %s", err.Error())
  619. }
  620. subdomain := dnsRecord.ExternalURL
  621. if ingressVal, ok := mergedValues["ingress"]; !ok {
  622. mergedValues["ingress"] = map[string]interface{}{
  623. "porter_hosts": []string{
  624. subdomain,
  625. },
  626. }
  627. } else {
  628. ingressValMap := ingressVal.(map[string]interface{})
  629. ingressValMap["porter_hosts"] = []string{
  630. subdomain,
  631. }
  632. }
  633. }
  634. }
  635. }
  636. return nil
  637. }
  638. func createDNSRecord(opts SubdomainCreateOpts) (*types.DNSRecord, error) {
  639. if opts.dnsClient == nil {
  640. return nil, fmt.Errorf("cannot create subdomain because dns client is nil")
  641. }
  642. endpoint, found, err := domain.GetNGINXIngressServiceIP(opts.k8sAgent.Clientset)
  643. if err != nil {
  644. return nil, err
  645. }
  646. if !found {
  647. return nil, fmt.Errorf("target cluster does not have nginx ingress")
  648. }
  649. createDomain := domain.CreateDNSRecordConfig{
  650. ReleaseName: opts.stackName,
  651. RootDomain: opts.appRootDomain,
  652. Endpoint: endpoint,
  653. }
  654. record := createDomain.NewDNSRecordForEndpoint()
  655. record, err = opts.dnsRepo.CreateDNSRecord(record)
  656. if err != nil {
  657. return nil, err
  658. }
  659. _record := domain.DNSRecord(*record)
  660. err = _record.CreateDomain(opts.dnsClient)
  661. if err != nil {
  662. return nil, err
  663. }
  664. return record.ToDNSRecordType(), nil
  665. }
  666. func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
  667. var res map[string]interface{}
  668. curr := obj
  669. for _, field := range fields {
  670. objField, ok := curr[field]
  671. if !ok {
  672. return nil, fmt.Errorf("%s not found", field)
  673. }
  674. res, ok = objField.(map[string]interface{})
  675. if !ok {
  676. return nil, fmt.Errorf("%s is not a nested object", field)
  677. }
  678. curr = res
  679. }
  680. return res, nil
  681. }
  682. func getHelmName(alias string, t string) string {
  683. var suffix string
  684. if t == "web" {
  685. suffix = "-web"
  686. } else if t == "worker" {
  687. suffix = "-wkr"
  688. } else if t == "job" {
  689. suffix = "-job"
  690. }
  691. return fmt.Sprintf("%s%s", alias, suffix)
  692. }
  693. func getChartTypeFromHelmName(name string) string {
  694. if strings.HasSuffix(name, "-web") {
  695. return "web"
  696. } else if strings.HasSuffix(name, "-wkr") {
  697. return "worker"
  698. } else if strings.HasSuffix(name, "-job") {
  699. return "job"
  700. }
  701. return ""
  702. }
  703. func getServiceNameAndTypeFromHelmName(name string) (string, string) {
  704. if strings.HasSuffix(name, "-web") {
  705. return strings.TrimSuffix(name, "-web"), "web"
  706. } else if strings.HasSuffix(name, "-wkr") {
  707. return strings.TrimSuffix(name, "-wkr"), "worker"
  708. } else if strings.HasSuffix(name, "-job") {
  709. return strings.TrimSuffix(name, "-job"), "job"
  710. }
  711. return "", ""
  712. }
  713. func attemptToGetImageInfoFromRelease(values map[string]interface{}) types.ImageInfo {
  714. imageInfo := types.ImageInfo{}
  715. if values == nil {
  716. return imageInfo
  717. }
  718. globalImage, err := getNestedMap(values, "global", "image")
  719. if err != nil {
  720. return imageInfo
  721. }
  722. repoVal, okRepo := globalImage["repository"]
  723. tagVal, okTag := globalImage["tag"]
  724. if okRepo && okTag {
  725. imageInfo.Repository = repoVal.(string)
  726. imageInfo.Tag = tagVal.(string)
  727. }
  728. return imageInfo
  729. }
  730. func attemptToGetImageInfoFromFullHelmValues(fullHelmValues string) (types.ImageInfo, error) {
  731. imageInfo := types.ImageInfo{}
  732. var values map[string]interface{}
  733. err := yaml.Unmarshal([]byte(fullHelmValues), &values)
  734. if err != nil {
  735. return imageInfo, fmt.Errorf("error unmarshaling full helm values to read image info: %w", err)
  736. }
  737. convertedValues := convertMap(values).(map[string]interface{})
  738. return attemptToGetImageInfoFromRelease(convertedValues), nil
  739. }
  740. func getStacksNestedMap(obj map[string]interface{}, fields ...string) ([]map[string]interface{}, error) {
  741. var res map[string]interface{}
  742. curr := obj
  743. for _, field := range fields {
  744. objField, ok := curr[field]
  745. if !ok {
  746. return nil, fmt.Errorf("%s not found", field)
  747. }
  748. res, ok = objField.(map[string]interface{})
  749. if !ok {
  750. return nil, fmt.Errorf("%s is not a nested object", field)
  751. }
  752. curr = res
  753. }
  754. syncedInterface, ok := curr["synced"]
  755. if !ok {
  756. return nil, fmt.Errorf("synced not found")
  757. }
  758. synced, ok := syncedInterface.([]interface{})
  759. if !ok {
  760. return nil, fmt.Errorf("synced is not a slice of interface{}")
  761. }
  762. result := make([]map[string]interface{}, len(synced))
  763. for i, v := range synced {
  764. mapElement, ok := v.(map[string]interface{})
  765. if !ok {
  766. return nil, fmt.Errorf("element %d in synced is not a map[string]interface{}", i)
  767. }
  768. result[i] = mapElement
  769. }
  770. return result, nil
  771. }
  772. func convertHelmValuesToPorterYaml(helmValues string) (*PorterStackYAML, error) {
  773. var values map[string]interface{}
  774. err := yaml.Unmarshal([]byte(helmValues), &values)
  775. if err != nil {
  776. return nil, err
  777. }
  778. services := make(map[string]*Service)
  779. for k, v := range values {
  780. if k == "global" {
  781. continue
  782. }
  783. serviceName, serviceType := getServiceNameAndTypeFromHelmName(k)
  784. if serviceName == "" {
  785. return nil, fmt.Errorf("invalid service key: %s. make sure that service key ends in either -web, -wkr, or -job", k)
  786. }
  787. config := convertMap(v).(map[string]interface{})
  788. var runCommand string
  789. if config["container"] != nil {
  790. containerMap := config["container"].(map[string]interface{})
  791. if containerMap["command"] != nil {
  792. runCommand = containerMap["command"].(string)
  793. }
  794. }
  795. services[serviceName] = &Service{
  796. Run: &runCommand,
  797. Config: config,
  798. Type: &serviceType,
  799. }
  800. }
  801. return &PorterStackYAML{
  802. Services: services,
  803. }, nil
  804. }
  805. // addLabelsToService always adds the default label to the service, and if envGroups is not empty, it adds the corresponding environment group label as well.
  806. func addLabelsToService(service *Service, envGroups []string, defaultLabelKey string) *Service {
  807. if _, ok := service.Config["labels"]; !ok {
  808. service.Config["labels"] = make(map[string]string)
  809. }
  810. if len(envGroups) != 0 {
  811. // delete the env group label so we can replace it
  812. if _, ok := service.Config["labels"].(map[string]any); ok {
  813. delete(service.Config["labels"].(map[string]any), environment_groups.LabelKey_LinkedEnvironmentGroup)
  814. }
  815. }
  816. switch service.Config["labels"].(type) {
  817. case map[string]string:
  818. service.Config["labels"].(map[string]string)[defaultLabelKey] = porter_app.LabelValue_PorterApplication
  819. if len(envGroups) != 0 {
  820. service.Config["labels"].(map[string]string)[environment_groups.LabelKey_LinkedEnvironmentGroup] = strings.Join(envGroups, ".")
  821. }
  822. case map[string]any:
  823. service.Config["labels"].(map[string]any)[defaultLabelKey] = porter_app.LabelValue_PorterApplication
  824. if len(envGroups) != 0 {
  825. service.Config["labels"].(map[string]any)[environment_groups.LabelKey_LinkedEnvironmentGroup] = strings.Join(envGroups, ".")
  826. }
  827. case any:
  828. if val, ok := service.Config["labels"].(string); ok {
  829. if val == "" {
  830. service.Config["labels"] = map[string]string{
  831. defaultLabelKey: porter_app.LabelValue_PorterApplication,
  832. }
  833. if len(envGroups) != 0 {
  834. service.Config["labels"].(map[string]string)[environment_groups.LabelKey_LinkedEnvironmentGroup] = strings.Join(envGroups, ".")
  835. }
  836. }
  837. }
  838. }
  839. if _, ok := service.Config["podLabels"]; !ok {
  840. service.Config["podLabels"] = make(map[string]string)
  841. }
  842. switch service.Config["podLabels"].(type) {
  843. case map[string]string:
  844. service.Config["podLabels"].(map[string]string)[defaultLabelKey] = porter_app.LabelValue_PorterApplication
  845. case map[string]any:
  846. service.Config["podLabels"].(map[string]any)[defaultLabelKey] = porter_app.LabelValue_PorterApplication
  847. case any:
  848. if val, ok := service.Config["podLabels"].(string); ok {
  849. if val == "" {
  850. service.Config["podLabels"] = map[string]string{
  851. defaultLabelKey: porter_app.LabelValue_PorterApplication,
  852. }
  853. }
  854. }
  855. }
  856. return service
  857. }
  858. func getServiceDeploymentMetadataFromValues(values map[string]interface{}, status types.PorterAppEventStatus) map[string]types.ServiceDeploymentMetadata {
  859. serviceDeploymentMap := make(map[string]types.ServiceDeploymentMetadata)
  860. for key := range values {
  861. if key != "global" {
  862. serviceName, serviceType := getServiceNameAndTypeFromHelmName(key)
  863. externalURI := getServiceExternalURIFromServiceValues(values[key].(map[string]interface{}))
  864. // jobs don't technically have a deployment, so hardcode the deployment status to success
  865. serviceStatus := status
  866. if serviceType == "job" {
  867. serviceStatus = types.PorterAppEventStatus_Success
  868. }
  869. serviceDeploymentMap[serviceName] = types.ServiceDeploymentMetadata{
  870. ExternalURI: externalURI,
  871. Status: serviceStatus,
  872. Type: serviceType,
  873. }
  874. }
  875. }
  876. return serviceDeploymentMap
  877. }
  878. func getServiceExternalURIFromServiceValues(serviceValues map[string]interface{}) string {
  879. ingressMap, err := getNestedMap(serviceValues, "ingress")
  880. if err == nil {
  881. enabledVal, enabledExists := ingressMap["enabled"]
  882. if enabledExists {
  883. enabled, eOK := enabledVal.(bool)
  884. if eOK && enabled {
  885. customDomVal, customDomExists := ingressMap["custom_domain"]
  886. if customDomExists {
  887. customDomain, cOK := customDomVal.(bool)
  888. if cOK && customDomain {
  889. hostsExists, hostsExistsOK := ingressMap["hosts"]
  890. if hostsExistsOK {
  891. if hosts, hostsOK := hostsExists.([]interface{}); hostsOK && len(hosts) == 1 {
  892. return hosts[0].(string)
  893. }
  894. }
  895. }
  896. }
  897. if porterHosts, ok := ingressMap["porter_hosts"].([]interface{}); ok && len(porterHosts) == 1 {
  898. return porterHosts[0].(string)
  899. }
  900. }
  901. }
  902. }
  903. return ""
  904. }