list.go 13 KB

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