list.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. package environment_groups
  2. import (
  3. "context"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "time"
  8. "github.com/porter-dev/porter/internal/kubernetes"
  9. "github.com/porter-dev/porter/internal/telemetry"
  10. appsv1 "k8s.io/api/apps/v1"
  11. batchv1 "k8s.io/api/batch/v1"
  12. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  13. )
  14. const (
  15. LabelKey_LinkedEnvironmentGroup = "porter.run/linked-environment-group"
  16. LabelKey_EnvironmentGroupVersion = "porter.run/environment-group-version"
  17. LabelKey_EnvironmentGroupName = "porter.run/environment-group-name"
  18. LabelKey_EnvironmentGroupType = "porter.run/environment-group-type"
  19. // LabelKey_PorterManaged is the label key signifying the resource is managed by porter
  20. LabelKey_PorterManaged = "porter.run/managed"
  21. LabelKey_DefaultAppEnvironment = "porter.run/default-app-environment"
  22. // LabelKey_DefaultAddonEnvironment is the label key signifying the resource is the default addon environment
  23. LabelKey_DefaultAddonEnvironment = "porter.run/default-addon-environment"
  24. // Namespace_EnvironmentGroups is the base namespace for storing all environment groups.
  25. // The configmaps and secrets here should be considered the source's of truth for a given version
  26. Namespace_EnvironmentGroups = "porter-env-group"
  27. // LabelKey_AppName is the label key for the app name
  28. LabelKey_AppName = "porter.run/app-name"
  29. )
  30. // EnvironmentGroup represents a ConfigMap in the porter-env-group namespace
  31. type EnvironmentGroup struct {
  32. // Type is the type of environment group
  33. Type string `json:"type"`
  34. // Name is the environment group name which can be found in the labels (LabelKey_EnvironmentGroupName) of the ConfigMap. This is NOT the configmap name
  35. Name string `json:"name"`
  36. // Version is the environment group version which can be found in the labels (LabelKey_EnvironmentGroupVersion) of the ConfigMap. This is NOT included in the configmap name
  37. Version int `json:"latest_version"`
  38. // Variables are non-secret values for the EnvironmentGroup. This usually will be a configmap
  39. Variables map[string]string `json:"variables,omitempty"`
  40. // SecretVariables are secret values for the EnvironmentGroup. This usually will be a Secret on the kubernetes cluster
  41. SecretVariables map[string]string `json:"secret_variables,omitempty"`
  42. // CreatedAt is only used for display purposes and is in UTC Unix time
  43. CreatedAtUTC time.Time `json:"created_at,omitempty"`
  44. // DefaultAppEnvironment is a boolean value that determines whether or not this environment group is the default environment group for an app
  45. DefaultAppEnvironment bool `json:"default_app_environment"`
  46. }
  47. type environmentGroupOptions struct {
  48. namespace string
  49. environmentGroupLabelName string
  50. environmentGroupLabelVersion int
  51. excludeDefaultAppEnvironmentGroups bool
  52. excludeDefaultAddonEnvironmentGroups bool
  53. }
  54. // EnvironmentGroupOption is a function that modifies ListEnvironmentGroups
  55. type EnvironmentGroupOption func(*environmentGroupOptions)
  56. // WithNamespace filters all environment groups in a given namespace
  57. func WithNamespace(namespace string) EnvironmentGroupOption {
  58. return func(opts *environmentGroupOptions) {
  59. opts.namespace = namespace
  60. }
  61. }
  62. // WithEnvironmentGroupName filters all environment groups by name
  63. func WithEnvironmentGroupName(name string) EnvironmentGroupOption {
  64. return func(opts *environmentGroupOptions) {
  65. opts.environmentGroupLabelName = name
  66. }
  67. }
  68. // WithEnvironmentGroupVersion filters all environment groups by version
  69. func WithEnvironmentGroupVersion(version int) EnvironmentGroupOption {
  70. return func(opts *environmentGroupOptions) {
  71. opts.environmentGroupLabelVersion = version
  72. }
  73. }
  74. // WithoutDefaultAppEnvironmentGroups includes default app environment groups in the list
  75. func WithoutDefaultAppEnvironmentGroups() EnvironmentGroupOption {
  76. return func(opts *environmentGroupOptions) {
  77. opts.excludeDefaultAppEnvironmentGroups = true
  78. }
  79. }
  80. // WithoutDefaultAddonEnvironmentGroups includes default addon environment groups in the list
  81. func WithoutDefaultAddonEnvironmentGroups() EnvironmentGroupOption {
  82. return func(opts *environmentGroupOptions) {
  83. opts.excludeDefaultAddonEnvironmentGroups = true
  84. }
  85. }
  86. // listEnvironmentGroups returns all environment groups stored in the provided namespace. If none is set, it will use the namespace "porter-env-group".
  87. // This method returns all secret values, which should never be returned out of this package. If you are trying to get the environment group values to return to the user,
  88. // use the exported ListEnvironmentGroups instead.
  89. func listEnvironmentGroups(ctx context.Context, a *kubernetes.Agent, listOpts ...EnvironmentGroupOption) ([]EnvironmentGroup, error) {
  90. ctx, span := telemetry.NewSpan(ctx, "list-environment-groups-private")
  91. defer span.End()
  92. var opts environmentGroupOptions
  93. for _, opt := range listOpts {
  94. opt(&opts)
  95. }
  96. if opts.namespace == "" {
  97. opts.namespace = Namespace_EnvironmentGroups
  98. }
  99. var labelSelectors []string
  100. if opts.environmentGroupLabelName != "" {
  101. labelSelectors = append(labelSelectors, fmt.Sprintf("%s=%s", LabelKey_EnvironmentGroupName, opts.environmentGroupLabelName))
  102. }
  103. if opts.environmentGroupLabelVersion != 0 {
  104. labelSelectors = append(labelSelectors, fmt.Sprintf("%s=%d", LabelKey_EnvironmentGroupVersion, opts.environmentGroupLabelVersion))
  105. }
  106. labelSelector := strings.Join(labelSelectors, ",")
  107. listOptions := metav1.ListOptions{
  108. LabelSelector: labelSelector,
  109. }
  110. telemetry.WithAttributes(span,
  111. telemetry.AttributeKV{Key: "namespace", Value: opts.namespace},
  112. telemetry.AttributeKV{Key: "label-selector", Value: labelSelector},
  113. )
  114. configMapListResp, err := a.Clientset.CoreV1().ConfigMaps(opts.namespace).List(ctx, listOptions)
  115. if err != nil {
  116. return nil, telemetry.Error(ctx, span, err, "unable to list environment group variables")
  117. }
  118. secretListResp, err := a.Clientset.CoreV1().Secrets(opts.namespace).List(ctx, listOptions)
  119. if err != nil {
  120. return nil, telemetry.Error(ctx, span, err, "unable to list environment groups secret varialbes")
  121. }
  122. // envGroupSet's key is the environment group's versioned name
  123. envGroupSet := make(map[string]EnvironmentGroup)
  124. for _, cm := range configMapListResp.Items {
  125. name, ok := cm.Labels[LabelKey_EnvironmentGroupName]
  126. if !ok {
  127. continue // missing name label, not an environment group
  128. }
  129. versionString, ok := cm.Labels[LabelKey_EnvironmentGroupVersion]
  130. if !ok {
  131. continue // missing version label, not an environment group
  132. }
  133. version, err := strconv.Atoi(versionString)
  134. if err != nil {
  135. continue // invalid version label as it should be an int, not an environment group
  136. }
  137. if opts.excludeDefaultAppEnvironmentGroups {
  138. value := cm.Labels[LabelKey_DefaultAppEnvironment]
  139. if value == "true" {
  140. continue // do not include default app environment groups
  141. }
  142. }
  143. if opts.excludeDefaultAddonEnvironmentGroups {
  144. value := cm.Labels[LabelKey_DefaultAddonEnvironment]
  145. if value == "true" {
  146. continue // do not include default addon environment groups
  147. }
  148. }
  149. if _, ok := envGroupSet[cm.Name]; !ok {
  150. envGroupSet[cm.Name] = EnvironmentGroup{}
  151. }
  152. envGroupSet[cm.Name] = EnvironmentGroup{
  153. Type: cm.Labels[LabelKey_EnvironmentGroupType],
  154. Name: name,
  155. Version: version,
  156. Variables: cm.Data,
  157. SecretVariables: envGroupSet[cm.Name].SecretVariables,
  158. CreatedAtUTC: cm.CreationTimestamp.Time.UTC(),
  159. DefaultAppEnvironment: cm.Labels[LabelKey_DefaultAppEnvironment] == "true",
  160. }
  161. }
  162. for _, secret := range secretListResp.Items {
  163. stringSecret := make(map[string]string)
  164. for k, v := range secret.Data {
  165. stringSecret[k] = string(v)
  166. }
  167. name, ok := secret.Labels[LabelKey_EnvironmentGroupName]
  168. if !ok {
  169. continue // missing name label, not an environment group
  170. }
  171. versionString, ok := secret.Labels[LabelKey_EnvironmentGroupVersion]
  172. if !ok {
  173. continue // missing version label, not an environment group
  174. }
  175. version, err := strconv.Atoi(versionString)
  176. if err != nil {
  177. continue // invalid version label as it should be an int, not an environment group
  178. }
  179. if opts.excludeDefaultAppEnvironmentGroups {
  180. value, ok := secret.Labels[LabelKey_DefaultAppEnvironment]
  181. if ok && value == "true" {
  182. continue // do not include default app environment groups
  183. }
  184. }
  185. if opts.excludeDefaultAddonEnvironmentGroups {
  186. value, ok := secret.Labels[LabelKey_DefaultAddonEnvironment]
  187. if ok && value == "true" {
  188. continue // do not include default addon environment groups
  189. }
  190. }
  191. if _, ok := envGroupSet[secret.Name]; !ok {
  192. envGroupSet[secret.Name] = EnvironmentGroup{}
  193. }
  194. envGroupSet[secret.Name] = EnvironmentGroup{
  195. Type: secret.Labels[LabelKey_EnvironmentGroupType],
  196. Name: name,
  197. Version: version,
  198. SecretVariables: stringSecret,
  199. Variables: envGroupSet[secret.Name].Variables,
  200. CreatedAtUTC: secret.CreationTimestamp.Time.UTC(),
  201. DefaultAppEnvironment: secret.Labels[LabelKey_DefaultAppEnvironment] == "true",
  202. }
  203. }
  204. var envGroups []EnvironmentGroup
  205. for _, envGroup := range envGroupSet {
  206. envGroups = append(envGroups, envGroup)
  207. }
  208. return envGroups, nil
  209. }
  210. // EnvGroupSecretDummyValue is the value that will be returned for secret variables in environment groups
  211. const EnvGroupSecretDummyValue = "********"
  212. // ListEnvironmentGroups returns all environment groups stored in the provided namespace. If none is set, it will use the namespace "porter-env-group".
  213. // This method replaces all secret values with a dummy value so that they are not exposed to the user. If you need access to the true secret values,
  214. // use the unexported listEnvironmentGroups instead.
  215. func ListEnvironmentGroups(ctx context.Context, a *kubernetes.Agent, listOpts ...EnvironmentGroupOption) ([]EnvironmentGroup, error) {
  216. ctx, span := telemetry.NewSpan(ctx, "list-environment-groups")
  217. defer span.End()
  218. envGroups, err := listEnvironmentGroups(ctx, a, listOpts...)
  219. if err != nil {
  220. return nil, telemetry.Error(ctx, span, err, "unable to list environment groups")
  221. }
  222. for _, envGroup := range envGroups {
  223. for k := range envGroup.SecretVariables {
  224. envGroup.SecretVariables[k] = EnvGroupSecretDummyValue
  225. }
  226. }
  227. return envGroups, nil
  228. }
  229. // LinkedPorterApplication represents an application which was linked to an environment group
  230. type LinkedPorterApplication struct {
  231. Name string
  232. Namespace string
  233. }
  234. func listLinkedAppsByUniqueAppLabel(environmentGroupName string, deployments []appsv1.Deployment, cronJobs []batchv1.CronJob) []LinkedPorterApplication {
  235. appsByName := make(map[string]LinkedPorterApplication)
  236. for _, d := range deployments {
  237. applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".")
  238. appName := d.Labels[LabelKey_AppName]
  239. for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups {
  240. if linkedEnvironmentGroup == environmentGroupName && appName != "" {
  241. appsByName[appName] = LinkedPorterApplication{
  242. Name: appName,
  243. Namespace: d.Namespace,
  244. }
  245. }
  246. }
  247. }
  248. for _, d := range cronJobs {
  249. applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".")
  250. appName := d.Labels[LabelKey_AppName]
  251. for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups {
  252. if linkedEnvironmentGroup == environmentGroupName && !strings.HasSuffix(d.Name, "predeploy") && appName != "" {
  253. appsByName[appName] = LinkedPorterApplication{
  254. Name: appName,
  255. Namespace: d.Namespace,
  256. }
  257. }
  258. }
  259. }
  260. var apps []LinkedPorterApplication
  261. for _, app := range appsByName {
  262. apps = append(apps, app)
  263. }
  264. return apps
  265. }
  266. func listLinkedAppsByUniqueNamespace(environmentGroupName string, deployments []appsv1.Deployment, cronJobs []batchv1.CronJob) []LinkedPorterApplication {
  267. var apps []LinkedPorterApplication
  268. for _, d := range deployments {
  269. applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".")
  270. for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups {
  271. if linkedEnvironmentGroup == environmentGroupName {
  272. apps = append(apps, LinkedPorterApplication{
  273. Name: d.Name,
  274. Namespace: d.Namespace,
  275. })
  276. }
  277. }
  278. }
  279. for _, d := range cronJobs {
  280. applicationsLinkedEnvironmentGroups := strings.Split(d.Labels[LabelKey_LinkedEnvironmentGroup], ".")
  281. for _, linkedEnvironmentGroup := range applicationsLinkedEnvironmentGroups {
  282. if linkedEnvironmentGroup == environmentGroupName {
  283. apps = append(apps, LinkedPorterApplication{
  284. Name: d.Name,
  285. Namespace: d.Namespace,
  286. })
  287. }
  288. }
  289. }
  290. return apps
  291. }
  292. // LinkedApplications lists all applications that are linked to a given environment group. Since there can be multiple linked environment groups we must check by the presence of a label on the deployment and job
  293. func LinkedApplications(ctx context.Context, a *kubernetes.Agent, environmentGroupName string, byUniqueNamespace bool) ([]LinkedPorterApplication, error) {
  294. ctx, span := telemetry.NewSpan(ctx, "list-linked-applications")
  295. defer span.End()
  296. if environmentGroupName == "" {
  297. return nil, telemetry.Error(ctx, span, nil, "environment group cannot be empty")
  298. }
  299. telemetry.WithAttributes(span, telemetry.AttributeKV{Key: "environment-group-name", Value: environmentGroupName})
  300. deployListResp, err := a.Clientset.AppsV1().Deployments(metav1.NamespaceAll).List(ctx,
  301. metav1.ListOptions{
  302. LabelSelector: LabelKey_LinkedEnvironmentGroup,
  303. })
  304. if err != nil {
  305. return nil, telemetry.Error(ctx, span, err, "unable to list linked deployment applications")
  306. }
  307. cronListResp, err := a.Clientset.BatchV1().CronJobs(metav1.NamespaceAll).List(ctx,
  308. metav1.ListOptions{
  309. LabelSelector: LabelKey_LinkedEnvironmentGroup,
  310. })
  311. if err != nil {
  312. return nil, telemetry.Error(ctx, span, err, "unable to list linked cronjob applications")
  313. }
  314. var apps []LinkedPorterApplication
  315. if byUniqueNamespace {
  316. apps = listLinkedAppsByUniqueNamespace(environmentGroupName, deployListResp.Items, cronListResp.Items)
  317. return apps, nil
  318. }
  319. apps = listLinkedAppsByUniqueAppLabel(environmentGroupName, deployListResp.Items, cronListResp.Items)
  320. return apps, nil
  321. }