parse.go 24 KB

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