list.go 13 KB

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