parse.go 32 KB

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