parse.go 29 KB

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