2
0

list.go 15 KB

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