parse.go 22 KB

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