parse.go 24 KB

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