list.go 12 KB

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