parse.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. package porter_app
  2. import (
  3. "fmt"
  4. "strconv"
  5. "strings"
  6. "github.com/porter-dev/porter/api/server/shared/config"
  7. "github.com/porter-dev/porter/api/types"
  8. "github.com/porter-dev/porter/cli/cmd/stack"
  9. "github.com/porter-dev/porter/internal/helm/loader"
  10. "github.com/porter-dev/porter/internal/integrations/powerdns"
  11. "github.com/porter-dev/porter/internal/kubernetes"
  12. "github.com/porter-dev/porter/internal/kubernetes/domain"
  13. "github.com/porter-dev/porter/internal/repository"
  14. "github.com/porter-dev/porter/internal/templater/utils"
  15. "github.com/stefanmcshane/helm/pkg/chart"
  16. "gopkg.in/yaml.v2"
  17. )
  18. type SubdomainCreateOpts struct {
  19. k8sAgent *kubernetes.Agent
  20. dnsRepo repository.DNSRecordRepository
  21. powerDnsClient *powerdns.Client
  22. appRootDomain string
  23. stackName string
  24. }
  25. type ParseConf struct {
  26. // PorterYaml is the raw porter yaml which is used to build the values + chart for helm upgrade
  27. PorterYaml []byte
  28. // ImageInfo contains the repository and tag of the image to use for the helm upgrade. Kept separate from the PorterYaml because the image info
  29. // is stored in the 'global' key of the values, which is not part of the porter yaml
  30. ImageInfo types.ImageInfo
  31. // ServerConfig is the server conf, used to find the default helm repo
  32. ServerConfig *config.Config
  33. // ProjectID
  34. ProjectID uint
  35. // UserUpdate used for synced env groups
  36. UserUpdate bool
  37. // EnvGroups used for synced env groups
  38. EnvGroups []string
  39. // Namespace used for synced env groups
  40. Namespace string
  41. // ExistingHelmValues is the existing values for the helm release, if it exists
  42. ExistingHelmValues map[string]interface{}
  43. // ExistingChartDependencies is the existing dependencies for the helm release, if it exists
  44. ExistingChartDependencies []*chart.Dependency
  45. // SubdomainCreateOpts contains the necessary information to create a subdomain if necessary
  46. SubdomainCreateOpts SubdomainCreateOpts
  47. // InjectLauncherToStartCommand is a flag to determine whether to prepend the launcher to the start command
  48. InjectLauncherToStartCommand bool
  49. // ShouldValidateHelmValues is a flag to determine whether to validate helm values
  50. ShouldValidateHelmValues bool
  51. // FullHelmValues if provided, override anything specified in porter.yaml. Used as an escape hatch for support
  52. FullHelmValues string
  53. }
  54. func parse(conf ParseConf) (*chart.Chart, map[string]interface{}, map[string]interface{}, error) {
  55. parsed := &stack.PorterStackYAML{}
  56. if conf.FullHelmValues != "" {
  57. parsedHelmValues, err := convertHelmValuesToPorterYaml(conf.FullHelmValues)
  58. if err != nil {
  59. return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing raw helm values", err)
  60. }
  61. parsed = parsedHelmValues
  62. } else {
  63. err := yaml.Unmarshal(conf.PorterYaml, parsed)
  64. if err != nil {
  65. return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing porter.yaml", err)
  66. }
  67. }
  68. synced_env := make([]*stack.SyncedEnvSection, 0)
  69. for i := range conf.EnvGroups {
  70. cm, _, err := conf.SubdomainCreateOpts.k8sAgent.GetLatestVersionedConfigMap(conf.EnvGroups[i], conf.Namespace)
  71. if err != nil {
  72. return nil, nil, nil, fmt.Errorf("%s: %w", "error building values from porter.yaml", err)
  73. }
  74. versionStr, ok := cm.ObjectMeta.Labels["version"]
  75. if !ok {
  76. return nil, nil, nil, fmt.Errorf("error extracting version from config map")
  77. }
  78. versionInt, err := strconv.Atoi(versionStr)
  79. if err != nil {
  80. return nil, nil, nil, fmt.Errorf("error converting version to int: %w", err)
  81. }
  82. version := uint(versionInt)
  83. newSection := &stack.SyncedEnvSection{
  84. Name: conf.EnvGroups[i],
  85. Version: version,
  86. }
  87. newSectionKeys := make([]stack.SyncedEnvSectionKey, 0)
  88. for key, val := range cm.Data {
  89. newSectionKeys = append(newSectionKeys, stack.SyncedEnvSectionKey{
  90. Name: key,
  91. Secret: strings.Contains(val, "PORTERSECRET"),
  92. })
  93. }
  94. newSection.Keys = newSectionKeys
  95. synced_env = append(synced_env, newSection)
  96. }
  97. application, err := stack.CreateAppFromFile(parsed)
  98. if err != nil {
  99. return nil, nil, nil, fmt.Errorf("%s: %w", "error parsing porter.yaml", err)
  100. }
  101. values, err := buildUmbrellaChartValues(application, synced_env, conf.ImageInfo, conf.ExistingHelmValues, conf.SubdomainCreateOpts, conf.InjectLauncherToStartCommand, conf.ShouldValidateHelmValues, conf.UserUpdate)
  102. if err != nil {
  103. return nil, nil, nil, fmt.Errorf("%s: %w", "error building values", err)
  104. }
  105. convertedValues := convertMap(values).(map[string]interface{})
  106. chart, err := buildUmbrellaChart(application, conf.ServerConfig, conf.ProjectID, conf.ExistingChartDependencies)
  107. if err != nil {
  108. return nil, nil, nil, fmt.Errorf("%s: %w", "error building chart", err)
  109. }
  110. // return the parsed release values for the release job chart, if they exist
  111. var preDeployJobValues map[string]interface{}
  112. if application.Release != nil && application.Release.Run != nil {
  113. preDeployJobValues = buildPreDeployJobChartValues(application.Release, application.Env, synced_env, conf.ImageInfo, conf.InjectLauncherToStartCommand, conf.ExistingHelmValues, strings.TrimSuffix(strings.TrimPrefix(conf.Namespace, "porter-stack-"), "")+"-r", conf.UserUpdate)
  114. }
  115. return chart, convertedValues, preDeployJobValues, nil
  116. }
  117. func buildUmbrellaChartValues(
  118. application *stack.Application,
  119. syncedEnv []*stack.SyncedEnvSection,
  120. imageInfo types.ImageInfo,
  121. existingValues map[string]interface{},
  122. opts SubdomainCreateOpts,
  123. injectLauncher bool,
  124. shouldValidateHelmValues bool,
  125. userUpdate bool,
  126. ) (map[string]interface{}, error) {
  127. values := make(map[string]interface{})
  128. if application.Services == nil {
  129. if existingValues == nil {
  130. return nil, fmt.Errorf("porter.yaml must contain at least one service, or pre-deploy must exist and have values")
  131. }
  132. }
  133. for name, service := range application.Services {
  134. serviceType := getType(name, service)
  135. defaultValues := getDefaultValues(service, application.Env, syncedEnv, serviceType, existingValues, name, userUpdate)
  136. convertedConfig := convertMap(service.Config).(map[string]interface{})
  137. helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
  138. // required to identify the chart type because of https://github.com/helm/helm/issues/9214
  139. helmName := getHelmName(name, serviceType)
  140. if existingValues != nil {
  141. if existingValues[helmName] != nil {
  142. existingValuesMap := existingValues[helmName].(map[string]interface{})
  143. helm_values = utils.DeepCoalesceValues(existingValuesMap, helm_values)
  144. }
  145. }
  146. validateErr := validateHelmValues(helm_values, shouldValidateHelmValues, serviceType)
  147. if validateErr != "" {
  148. return nil, fmt.Errorf("error validating service \"%s\": %s", name, validateErr)
  149. }
  150. err := createSubdomainIfRequired(helm_values, opts) // modifies helm_values to add subdomains if necessary
  151. if err != nil {
  152. return nil, err
  153. }
  154. // just in case this slips by
  155. if serviceType == "web" {
  156. if helm_values["ingress"] == nil {
  157. helm_values["ingress"] = map[string]interface{}{
  158. "enabled": false,
  159. }
  160. }
  161. }
  162. values[helmName] = helm_values
  163. }
  164. // add back in the existing services that were not overwritten
  165. for k, v := range existingValues {
  166. if values[k] == nil {
  167. values[k] = v
  168. }
  169. }
  170. // prepend launcher to all start commands if we need to
  171. for _, v := range values {
  172. if serviceValues, ok := v.(map[string]interface{}); ok {
  173. if serviceValues["container"] != nil {
  174. containerMap := serviceValues["container"].(map[string]interface{})
  175. if containerMap["command"] != nil {
  176. command := containerMap["command"].(string)
  177. if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
  178. containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
  179. }
  180. }
  181. }
  182. }
  183. }
  184. if imageInfo.Repository != "" && imageInfo.Tag != "" {
  185. values["global"] = map[string]interface{}{
  186. "image": map[string]interface{}{
  187. "repository": imageInfo.Repository,
  188. "tag": imageInfo.Tag,
  189. },
  190. }
  191. }
  192. return values, nil
  193. }
  194. // we can add to this function up later or use an alternative
  195. func validateHelmValues(values map[string]interface{}, shouldValidateHelmValues bool, appType string) string {
  196. if shouldValidateHelmValues {
  197. // validate port for web services
  198. if appType == "web" {
  199. containerMap, err := getNestedMap(values, "container")
  200. if err != nil {
  201. return "error checking port: misformatted values"
  202. } else {
  203. portVal, portExists := containerMap["port"]
  204. if portExists {
  205. portStr, pOK := portVal.(string)
  206. if !pOK {
  207. return "error checking port: no port in container"
  208. }
  209. port, err := strconv.Atoi(portStr)
  210. if err != nil || port < 1024 || port > 65535 {
  211. return "port must be a number between 1024 and 65535"
  212. }
  213. } else {
  214. return "port must be specified for web services"
  215. }
  216. }
  217. }
  218. }
  219. return ""
  220. }
  221. func buildPreDeployJobChartValues(release *stack.Service, env map[string]string, synced_env []*stack.SyncedEnvSection, imageInfo types.ImageInfo, injectLauncher bool, existingValues map[string]interface{}, name string, userUpdate bool) map[string]interface{} {
  222. defaultValues := getDefaultValues(release, env, synced_env, "job", existingValues, name+"-r", userUpdate)
  223. convertedConfig := convertMap(release.Config).(map[string]interface{})
  224. helm_values := utils.DeepCoalesceValues(defaultValues, convertedConfig)
  225. if imageInfo.Repository != "" && imageInfo.Tag != "" {
  226. helm_values["image"] = map[string]interface{}{
  227. "repository": imageInfo.Repository,
  228. "tag": imageInfo.Tag,
  229. }
  230. }
  231. // prepend launcher if we need to
  232. if helm_values["container"] != nil {
  233. containerMap := helm_values["container"].(map[string]interface{})
  234. if containerMap["command"] != nil {
  235. command := containerMap["command"].(string)
  236. if injectLauncher && !strings.HasPrefix(command, "launcher") && !strings.HasPrefix(command, "/cnb/lifecycle/launcher") {
  237. containerMap["command"] = fmt.Sprintf("/cnb/lifecycle/launcher %s", command)
  238. }
  239. }
  240. }
  241. return helm_values
  242. }
  243. func getType(name string, service *stack.Service) string {
  244. if service.Type != nil {
  245. return *service.Type
  246. }
  247. if strings.Contains(name, "web") {
  248. return "web"
  249. }
  250. if strings.Contains(name, "job") {
  251. return "job"
  252. }
  253. return "worker"
  254. }
  255. func getDefaultValues(service *stack.Service, env map[string]string, synced_env []*stack.SyncedEnvSection, appType string, existingValues map[string]interface{}, name string, userUpdate bool) map[string]interface{} {
  256. var defaultValues map[string]interface{}
  257. var runCommand string
  258. if service.Run != nil {
  259. runCommand = *service.Run
  260. }
  261. var syncedEnvs []map[string]interface{}
  262. envConf, err := getStacksNestedMap(existingValues, name+"-"+appType, "container", "env")
  263. if !userUpdate && err == nil {
  264. syncedEnvs = envConf
  265. } else {
  266. syncedEnvs = deconstructSyncedEnvs(synced_env, env)
  267. }
  268. defaultValues = map[string]interface{}{
  269. "container": map[string]interface{}{
  270. "command": runCommand,
  271. "env": map[string]interface{}{
  272. "normal": CopyEnv(env),
  273. "synced": syncedEnvs,
  274. },
  275. },
  276. }
  277. return defaultValues
  278. }
  279. func deconstructSyncedEnvs(synced_env []*stack.SyncedEnvSection, env map[string]string) []map[string]interface{} {
  280. synced := make([]map[string]interface{}, 0)
  281. for _, group := range synced_env {
  282. keys := make([]map[string]interface{}, 0)
  283. for _, key := range group.Keys {
  284. if _, exists := env[key.Name]; !exists {
  285. // Only include keys not present in env
  286. keys = append(keys, map[string]interface{}{
  287. "name": key.Name,
  288. "secret": key.Secret,
  289. })
  290. }
  291. }
  292. syncedGroup := map[string]interface{}{
  293. "keys": keys,
  294. "name": group.Name,
  295. "version": group.Version,
  296. }
  297. synced = append(synced, syncedGroup)
  298. }
  299. return synced
  300. }
  301. func buildUmbrellaChart(application *stack.Application, config *config.Config, projectID uint, existingDependencies []*chart.Dependency) (*chart.Chart, error) {
  302. deps := make([]*chart.Dependency, 0)
  303. for alias, service := range application.Services {
  304. var serviceType string
  305. if existingDependencies != nil {
  306. for _, dep := range existingDependencies {
  307. // 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
  308. if strings.HasPrefix(dep.Alias, fmt.Sprintf("%s-", alias)) && (strings.HasSuffix(dep.Alias, "-web") || strings.HasSuffix(dep.Alias, "-wkr") || strings.HasSuffix(dep.Alias, "-job")) {
  309. serviceType = getChartTypeFromHelmName(dep.Alias)
  310. if serviceType == "" {
  311. return nil, fmt.Errorf("unable to determine type of existing dependency")
  312. }
  313. }
  314. }
  315. // this is a new app, so we need to get the type from the app name or type
  316. if serviceType == "" {
  317. serviceType = getType(alias, service)
  318. }
  319. } else {
  320. serviceType = getType(alias, service)
  321. }
  322. selectedRepo := config.ServerConf.DefaultApplicationHelmRepoURL
  323. selectedVersion, err := getLatestTemplateVersion(serviceType, config, projectID)
  324. if err != nil {
  325. return nil, err
  326. }
  327. helmName := getHelmName(alias, serviceType)
  328. deps = append(deps, &chart.Dependency{
  329. Name: serviceType,
  330. Alias: helmName,
  331. Version: selectedVersion,
  332. Repository: selectedRepo,
  333. })
  334. }
  335. // add in the existing dependencies that were not overwritten
  336. for _, dep := range existingDependencies {
  337. if !dependencyExists(deps, dep) {
  338. // have to repair the dependency name because of https://github.com/helm/helm/issues/9214
  339. if strings.HasSuffix(dep.Name, "-web") || strings.HasSuffix(dep.Name, "-wkr") || strings.HasSuffix(dep.Name, "-job") {
  340. dep.Name = getChartTypeFromHelmName(dep.Name)
  341. }
  342. deps = append(deps, dep)
  343. }
  344. }
  345. chart, err := createChartFromDependencies(deps)
  346. if err != nil {
  347. return nil, err
  348. }
  349. return chart, nil
  350. }
  351. func dependencyExists(deps []*chart.Dependency, dep *chart.Dependency) bool {
  352. for _, d := range deps {
  353. if d.Alias == dep.Alias {
  354. return true
  355. }
  356. }
  357. return false
  358. }
  359. func createChartFromDependencies(deps []*chart.Dependency) (*chart.Chart, error) {
  360. metadata := &chart.Metadata{
  361. Name: "umbrella",
  362. Description: "Web application that is exposed to external traffic.",
  363. Version: "0.96.0",
  364. APIVersion: "v2",
  365. Home: "https://getporter.dev/",
  366. Icon: "https://user-images.githubusercontent.com/65516095/111255214-07d3da80-85ed-11eb-99e2-fddcbdb99bdb.png",
  367. Keywords: []string{
  368. "porter",
  369. "application",
  370. "service",
  371. "umbrella",
  372. },
  373. Type: "application",
  374. Dependencies: deps,
  375. }
  376. // create a new chart object with the metadata
  377. c := &chart.Chart{
  378. Metadata: metadata,
  379. }
  380. return c, nil
  381. }
  382. func getLatestTemplateVersion(templateName string, config *config.Config, projectID uint) (string, error) {
  383. repoIndex, err := loader.LoadRepoIndexPublic(config.ServerConf.DefaultApplicationHelmRepoURL)
  384. if err != nil {
  385. return "", fmt.Errorf("%s: %w", "unable to load porter chart repo", err)
  386. }
  387. templates := loader.RepoIndexToPorterChartList(repoIndex, config.ServerConf.DefaultApplicationHelmRepoURL)
  388. if err != nil {
  389. return "", fmt.Errorf("%s: %w", "unable to load porter chart list", err)
  390. }
  391. var version string
  392. // find the matching template name
  393. for _, template := range templates {
  394. if templateName == template.Name {
  395. version = template.Versions[0]
  396. break
  397. }
  398. }
  399. if version == "" {
  400. return "", fmt.Errorf("matching template version not found")
  401. }
  402. return version, nil
  403. }
  404. func convertMap(m interface{}) interface{} {
  405. switch m := m.(type) {
  406. case map[string]interface{}:
  407. for k, v := range m {
  408. m[k] = convertMap(v)
  409. }
  410. case map[interface{}]interface{}:
  411. result := map[string]interface{}{}
  412. for k, v := range m {
  413. result[k.(string)] = convertMap(v)
  414. }
  415. return result
  416. case []interface{}:
  417. for i, v := range m {
  418. m[i] = convertMap(v)
  419. }
  420. }
  421. return m
  422. }
  423. func CopyEnv(env map[string]string) map[string]interface{} {
  424. envCopy := make(map[string]interface{})
  425. if env == nil {
  426. return envCopy
  427. }
  428. for k, v := range env {
  429. if k == "" || v == "" {
  430. continue
  431. }
  432. envCopy[k] = v
  433. }
  434. return envCopy
  435. }
  436. func createSubdomainIfRequired(
  437. mergedValues map[string]interface{},
  438. opts SubdomainCreateOpts,
  439. ) error {
  440. // look for ingress.enabled and no custom domains set
  441. ingressMap, err := getNestedMap(mergedValues, "ingress")
  442. if err == nil {
  443. enabledVal, enabledExists := ingressMap["enabled"]
  444. if enabledExists {
  445. enabled, eOK := enabledVal.(bool)
  446. if eOK && enabled {
  447. // if custom domain, we don't need to create a subdomain
  448. customDomVal, customDomExists := ingressMap["custom_domain"]
  449. if customDomExists {
  450. customDomain, cOK := customDomVal.(bool)
  451. if cOK && customDomain {
  452. return nil
  453. }
  454. }
  455. // subdomain already exists, no need to create one
  456. if porterHosts, ok := ingressMap["porter_hosts"].([]interface{}); ok && len(porterHosts) > 0 {
  457. return nil
  458. }
  459. // in the case of ingress enabled but no custom domain, create subdomain
  460. dnsRecord, err := createDNSRecord(opts)
  461. if err != nil {
  462. return fmt.Errorf("error creating subdomain: %s", err.Error())
  463. }
  464. subdomain := dnsRecord.ExternalURL
  465. if ingressVal, ok := mergedValues["ingress"]; !ok {
  466. mergedValues["ingress"] = map[string]interface{}{
  467. "porter_hosts": []string{
  468. subdomain,
  469. },
  470. }
  471. } else {
  472. ingressValMap := ingressVal.(map[string]interface{})
  473. ingressValMap["porter_hosts"] = []string{
  474. subdomain,
  475. }
  476. }
  477. }
  478. }
  479. }
  480. return nil
  481. }
  482. func createDNSRecord(opts SubdomainCreateOpts) (*types.DNSRecord, error) {
  483. if opts.powerDnsClient == nil {
  484. return nil, fmt.Errorf("cannot create subdomain because powerdns client is nil")
  485. }
  486. endpoint, found, err := domain.GetNGINXIngressServiceIP(opts.k8sAgent.Clientset)
  487. if err != nil {
  488. return nil, err
  489. }
  490. if !found {
  491. return nil, fmt.Errorf("target cluster does not have nginx ingress")
  492. }
  493. createDomain := domain.CreateDNSRecordConfig{
  494. ReleaseName: opts.stackName,
  495. RootDomain: opts.appRootDomain,
  496. Endpoint: endpoint,
  497. }
  498. record := createDomain.NewDNSRecordForEndpoint()
  499. record, err = opts.dnsRepo.CreateDNSRecord(record)
  500. if err != nil {
  501. return nil, err
  502. }
  503. _record := domain.DNSRecord(*record)
  504. err = _record.CreateDomain(opts.powerDnsClient)
  505. if err != nil {
  506. return nil, err
  507. }
  508. return record.ToDNSRecordType(), nil
  509. }
  510. func getNestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, error) {
  511. var res map[string]interface{}
  512. curr := obj
  513. for _, field := range fields {
  514. objField, ok := curr[field]
  515. if !ok {
  516. return nil, fmt.Errorf("%s not found", field)
  517. }
  518. res, ok = objField.(map[string]interface{})
  519. if !ok {
  520. return nil, fmt.Errorf("%s is not a nested object", field)
  521. }
  522. curr = res
  523. }
  524. return res, nil
  525. }
  526. func getHelmName(alias string, t string) string {
  527. var suffix string
  528. if t == "web" {
  529. suffix = "-web"
  530. } else if t == "worker" {
  531. suffix = "-wkr"
  532. } else if t == "job" {
  533. suffix = "-job"
  534. }
  535. return fmt.Sprintf("%s%s", alias, suffix)
  536. }
  537. func getChartTypeFromHelmName(name string) string {
  538. if strings.HasSuffix(name, "-web") {
  539. return "web"
  540. } else if strings.HasSuffix(name, "-wkr") {
  541. return "worker"
  542. } else if strings.HasSuffix(name, "-job") {
  543. return "job"
  544. }
  545. return ""
  546. }
  547. func getServiceNameAndTypeFromHelmName(name string) (string, string) {
  548. if strings.HasSuffix(name, "-web") {
  549. return strings.TrimSuffix(name, "-web"), "web"
  550. } else if strings.HasSuffix(name, "-wkr") {
  551. return strings.TrimSuffix(name, "-wkr"), "worker"
  552. } else if strings.HasSuffix(name, "-job") {
  553. return strings.TrimSuffix(name, "-job"), "job"
  554. }
  555. return "", ""
  556. }
  557. func attemptToGetImageInfoFromRelease(values map[string]interface{}) types.ImageInfo {
  558. imageInfo := types.ImageInfo{}
  559. if values == nil {
  560. return imageInfo
  561. }
  562. globalImage, err := getNestedMap(values, "global", "image")
  563. if err != nil {
  564. return imageInfo
  565. }
  566. repoVal, okRepo := globalImage["repository"]
  567. tagVal, okTag := globalImage["tag"]
  568. if okRepo && okTag {
  569. imageInfo.Repository = repoVal.(string)
  570. imageInfo.Tag = tagVal.(string)
  571. }
  572. return imageInfo
  573. }
  574. func attemptToGetImageInfoFromFullHelmValues(fullHelmValues string) (types.ImageInfo, error) {
  575. imageInfo := types.ImageInfo{}
  576. var values map[string]interface{}
  577. err := yaml.Unmarshal([]byte(fullHelmValues), &values)
  578. if err != nil {
  579. return imageInfo, fmt.Errorf("error unmarshaling full helm values to read image info: %w", err)
  580. }
  581. convertedValues := convertMap(values).(map[string]interface{})
  582. return attemptToGetImageInfoFromRelease(convertedValues), nil
  583. }
  584. func getStacksNestedMap(obj map[string]interface{}, fields ...string) ([]map[string]interface{}, error) {
  585. var res map[string]interface{}
  586. curr := obj
  587. for _, field := range fields {
  588. objField, ok := curr[field]
  589. if !ok {
  590. return nil, fmt.Errorf("%s not found", field)
  591. }
  592. res, ok = objField.(map[string]interface{})
  593. if !ok {
  594. return nil, fmt.Errorf("%s is not a nested object", field)
  595. }
  596. curr = res
  597. }
  598. syncedInterface, ok := curr["synced"]
  599. if !ok {
  600. return nil, fmt.Errorf("synced not found")
  601. }
  602. synced, ok := syncedInterface.([]interface{})
  603. if !ok {
  604. return nil, fmt.Errorf("synced is not a slice of interface{}")
  605. }
  606. result := make([]map[string]interface{}, len(synced))
  607. for i, v := range synced {
  608. mapElement, ok := v.(map[string]interface{})
  609. if !ok {
  610. return nil, fmt.Errorf("element %d in synced is not a map[string]interface{}", i)
  611. }
  612. result[i] = mapElement
  613. }
  614. return result, nil
  615. }
  616. func convertHelmValuesToPorterYaml(helmValues string) (*stack.PorterStackYAML, error) {
  617. var values map[string]interface{}
  618. err := yaml.Unmarshal([]byte(helmValues), &values)
  619. if err != nil {
  620. return nil, err
  621. }
  622. services := make(map[string]*stack.Service)
  623. for k, v := range values {
  624. if k == "global" {
  625. continue
  626. }
  627. serviceName, serviceType := getServiceNameAndTypeFromHelmName(k)
  628. if serviceName == "" {
  629. return nil, fmt.Errorf("invalid service key: %s. make sure that service key ends in either -web, -wkr, or -job", k)
  630. }
  631. services[serviceName] = &stack.Service{
  632. Config: convertMap(v).(map[string]interface{}),
  633. Type: &serviceType,
  634. }
  635. }
  636. return &stack.PorterStackYAML{
  637. Services: services,
  638. }, nil
  639. }