parse.go 29 KB

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