parse.go 23 KB

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