list.go 13 KB

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