GitOps for k8s
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

resourcekinds.go 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. package kubernetes
  2. import (
  3. "context"
  4. "strings"
  5. hr_v1 "github.com/fluxcd/helm-operator/pkg/apis/helm.fluxcd.io/v1"
  6. apiapps "k8s.io/api/apps/v1"
  7. apibatch "k8s.io/api/batch/v1beta1"
  8. apiv1 "k8s.io/api/core/v1"
  9. meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  10. "github.com/fluxcd/flux/cluster"
  11. kresource "github.com/fluxcd/flux/cluster/kubernetes/resource"
  12. "github.com/fluxcd/flux/image"
  13. hr_v1beta1 "github.com/fluxcd/flux/integrations/apis/flux.weave.works/v1beta1"
  14. fhr_v1alpha2 "github.com/fluxcd/flux/integrations/apis/helm.integrations.flux.weave.works/v1alpha2"
  15. "github.com/fluxcd/flux/policy"
  16. "github.com/fluxcd/flux/resource"
  17. )
  18. // AntecedentAnnotation is an annotation on a resource indicating that
  19. // the cause of that resource (indirectly, via a Helm release) is a
  20. // HelmRelease. We use this rather than the `OwnerReference` type
  21. // built into Kubernetes so that there are no garbage-collection
  22. // implications. The value is expected to be a serialised
  23. // `resource.ID`.
  24. const AntecedentAnnotation = "flux.weave.works/antecedent"
  25. /////////////////////////////////////////////////////////////////////////////
  26. // Kind registry
  27. type resourceKind interface {
  28. getWorkload(ctx context.Context, c *Cluster, namespace, name string) (workload, error)
  29. getWorkloads(ctx context.Context, c *Cluster, namespace string) ([]workload, error)
  30. }
  31. var (
  32. resourceKinds = make(map[string]resourceKind)
  33. )
  34. func init() {
  35. resourceKinds["cronjob"] = &cronJobKind{}
  36. resourceKinds["daemonset"] = &daemonSetKind{}
  37. resourceKinds["deployment"] = &deploymentKind{}
  38. resourceKinds["statefulset"] = &statefulSetKind{}
  39. resourceKinds["helmrelease"] = &helmReleaseKind{}
  40. resourceKinds["fluxhelmrelease"] = &fluxHelmReleaseKind{}
  41. }
  42. type workload struct {
  43. k8sObject
  44. status string
  45. rollout cluster.RolloutStatus
  46. syncError error
  47. podTemplate apiv1.PodTemplateSpec
  48. }
  49. func (w workload) toClusterWorkload(resourceID resource.ID) cluster.Workload {
  50. var clusterContainers []resource.Container
  51. var excuse string
  52. for _, container := range w.podTemplate.Spec.Containers {
  53. ref, err := image.ParseRef(container.Image)
  54. if err != nil {
  55. clusterContainers = nil
  56. excuse = err.Error()
  57. break
  58. }
  59. clusterContainers = append(clusterContainers, resource.Container{Name: container.Name, Image: ref})
  60. }
  61. for _, container := range w.podTemplate.Spec.InitContainers {
  62. ref, err := image.ParseRef(container.Image)
  63. if err != nil {
  64. clusterContainers = nil
  65. excuse = err.Error()
  66. break
  67. }
  68. clusterContainers = append(clusterContainers, resource.Container{Name: container.Name, Image: ref})
  69. }
  70. var antecedent resource.ID
  71. if ante, ok := w.GetAnnotations()[AntecedentAnnotation]; ok {
  72. id, err := resource.ParseID(ante)
  73. if err == nil {
  74. antecedent = id
  75. }
  76. }
  77. var policies policy.Set
  78. for k, v := range w.GetAnnotations() {
  79. if strings.HasPrefix(k, kresource.PolicyPrefix) {
  80. p := strings.TrimPrefix(k, kresource.PolicyPrefix)
  81. if v == "true" {
  82. policies = policies.Add(policy.Policy(p))
  83. } else {
  84. policies = policies.Set(policy.Policy(p), v)
  85. }
  86. }
  87. }
  88. return cluster.Workload{
  89. ID: resourceID,
  90. Status: w.status,
  91. Rollout: w.rollout,
  92. SyncError: w.syncError,
  93. Antecedent: antecedent,
  94. Labels: w.GetLabels(),
  95. Policies: policies,
  96. Containers: cluster.ContainersOrExcuse{Containers: clusterContainers, Excuse: excuse},
  97. }
  98. }
  99. /////////////////////////////////////////////////////////////////////////////
  100. // extensions/v1beta1 Deployment
  101. type deploymentKind struct{}
  102. func (dk *deploymentKind) getWorkload(ctx context.Context, c *Cluster, namespace, name string) (workload, error) {
  103. if err := ctx.Err(); err != nil {
  104. return workload{}, err
  105. }
  106. deployment, err := c.client.AppsV1().Deployments(namespace).Get(name, meta_v1.GetOptions{})
  107. if err != nil {
  108. return workload{}, err
  109. }
  110. return makeDeploymentWorkload(deployment), nil
  111. }
  112. func (dk *deploymentKind) getWorkloads(ctx context.Context, c *Cluster, namespace string) ([]workload, error) {
  113. if err := ctx.Err(); err != nil {
  114. return nil, err
  115. }
  116. deployments, err := c.client.AppsV1().Deployments(namespace).List(meta_v1.ListOptions{})
  117. if err != nil {
  118. return nil, err
  119. }
  120. var workloads []workload
  121. for i := range deployments.Items {
  122. workloads = append(workloads, makeDeploymentWorkload(&deployments.Items[i]))
  123. }
  124. return workloads, nil
  125. }
  126. // Deployment may get stuck trying to deploy its newest ReplicaSet without ever completing.
  127. // One way to detect this condition is to specify a deadline parameter in Deployment spec:
  128. // .spec.progressDeadlineSeconds
  129. // See https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#failed-deployment
  130. func deploymentErrors(d *apiapps.Deployment) []string {
  131. var errs []string
  132. for _, cond := range d.Status.Conditions {
  133. if (cond.Type == apiapps.DeploymentProgressing && cond.Status == apiv1.ConditionFalse) ||
  134. (cond.Type == apiapps.DeploymentReplicaFailure && cond.Status == apiv1.ConditionTrue) {
  135. errs = append(errs, cond.Message)
  136. }
  137. }
  138. return errs
  139. }
  140. func makeDeploymentWorkload(deployment *apiapps.Deployment) workload {
  141. var status string
  142. objectMeta, deploymentStatus := deployment.ObjectMeta, deployment.Status
  143. status = cluster.StatusStarted
  144. rollout := cluster.RolloutStatus{
  145. Desired: *deployment.Spec.Replicas,
  146. Updated: deploymentStatus.UpdatedReplicas,
  147. Ready: deploymentStatus.ReadyReplicas,
  148. Available: deploymentStatus.AvailableReplicas,
  149. Outdated: deploymentStatus.Replicas - deploymentStatus.UpdatedReplicas,
  150. Messages: deploymentErrors(deployment),
  151. }
  152. if deploymentStatus.ObservedGeneration >= objectMeta.Generation {
  153. // the definition has been updated; now let's see about the replicas
  154. status = cluster.StatusUpdating
  155. if rollout.Updated == rollout.Desired && rollout.Available == rollout.Desired && rollout.Outdated == 0 {
  156. status = cluster.StatusReady
  157. }
  158. if len(rollout.Messages) != 0 {
  159. status = cluster.StatusError
  160. }
  161. }
  162. // apiVersion & kind must be set, since TypeMeta is not populated
  163. deployment.APIVersion = "apps/v1"
  164. deployment.Kind = "Deployment"
  165. return workload{
  166. status: status,
  167. rollout: rollout,
  168. podTemplate: deployment.Spec.Template,
  169. k8sObject: deployment}
  170. }
  171. /////////////////////////////////////////////////////////////////////////////
  172. // extensions/v1beta daemonset
  173. type daemonSetKind struct{}
  174. func (dk *daemonSetKind) getWorkload(ctx context.Context, c *Cluster, namespace, name string) (workload, error) {
  175. if err := ctx.Err(); err != nil {
  176. return workload{}, err
  177. }
  178. daemonSet, err := c.client.AppsV1().DaemonSets(namespace).Get(name, meta_v1.GetOptions{})
  179. if err != nil {
  180. return workload{}, err
  181. }
  182. return makeDaemonSetWorkload(daemonSet), nil
  183. }
  184. func (dk *daemonSetKind) getWorkloads(ctx context.Context, c *Cluster, namespace string) ([]workload, error) {
  185. if err := ctx.Err(); err != nil {
  186. return nil, err
  187. }
  188. daemonSets, err := c.client.AppsV1().DaemonSets(namespace).List(meta_v1.ListOptions{})
  189. if err != nil {
  190. return nil, err
  191. }
  192. var workloads []workload
  193. for i := range daemonSets.Items {
  194. workloads = append(workloads, makeDaemonSetWorkload(&daemonSets.Items[i]))
  195. }
  196. return workloads, nil
  197. }
  198. func makeDaemonSetWorkload(daemonSet *apiapps.DaemonSet) workload {
  199. var status string
  200. objectMeta, daemonSetStatus := daemonSet.ObjectMeta, daemonSet.Status
  201. status = cluster.StatusStarted
  202. rollout := cluster.RolloutStatus{
  203. Desired: daemonSetStatus.DesiredNumberScheduled,
  204. Updated: daemonSetStatus.UpdatedNumberScheduled,
  205. Ready: daemonSetStatus.NumberReady,
  206. Available: daemonSetStatus.NumberAvailable,
  207. Outdated: daemonSetStatus.CurrentNumberScheduled - daemonSetStatus.UpdatedNumberScheduled,
  208. // TODO Add Messages after "TODO: Add valid condition types of a DaemonSet" fixed in
  209. // https://github.com/kubernetes/kubernetes/blob/f3e0750754ebeea4ea8e0d452cbaf55426751d12/pkg/apis/extensions/types.go#L434
  210. }
  211. if daemonSetStatus.ObservedGeneration >= objectMeta.Generation {
  212. // the definition has been updated; now let's see about the replicas
  213. status = cluster.StatusUpdating
  214. if rollout.Updated == rollout.Desired && rollout.Available == rollout.Desired && rollout.Outdated == 0 {
  215. status = cluster.StatusReady
  216. }
  217. }
  218. // apiVersion & kind must be set, since TypeMeta is not populated
  219. daemonSet.APIVersion = "apps/v1"
  220. daemonSet.Kind = "DaemonSet"
  221. return workload{
  222. status: status,
  223. rollout: rollout,
  224. podTemplate: daemonSet.Spec.Template,
  225. k8sObject: daemonSet}
  226. }
  227. /////////////////////////////////////////////////////////////////////////////
  228. // apps/v1beta1 StatefulSet
  229. type statefulSetKind struct{}
  230. func (dk *statefulSetKind) getWorkload(ctx context.Context, c *Cluster, namespace, name string) (workload, error) {
  231. if err := ctx.Err(); err != nil {
  232. return workload{}, err
  233. }
  234. statefulSet, err := c.client.AppsV1().StatefulSets(namespace).Get(name, meta_v1.GetOptions{})
  235. if err != nil {
  236. return workload{}, err
  237. }
  238. return makeStatefulSetWorkload(statefulSet), nil
  239. }
  240. func (dk *statefulSetKind) getWorkloads(ctx context.Context, c *Cluster, namespace string) ([]workload, error) {
  241. if err := ctx.Err(); err != nil {
  242. return nil, err
  243. }
  244. statefulSets, err := c.client.AppsV1().StatefulSets(namespace).List(meta_v1.ListOptions{})
  245. if err != nil {
  246. return nil, err
  247. }
  248. var workloads []workload
  249. for i := range statefulSets.Items {
  250. workloads = append(workloads, makeStatefulSetWorkload(&statefulSets.Items[i]))
  251. }
  252. return workloads, nil
  253. }
  254. func makeStatefulSetWorkload(statefulSet *apiapps.StatefulSet) workload {
  255. var status string
  256. objectMeta, statefulSetStatus := statefulSet.ObjectMeta, statefulSet.Status
  257. status = cluster.StatusStarted
  258. rollout := cluster.RolloutStatus{
  259. Ready: statefulSetStatus.ReadyReplicas,
  260. // There is no Available parameter for statefulset, so use Ready instead
  261. Available: statefulSetStatus.ReadyReplicas,
  262. // TODO Add Messages after "TODO: Add valid condition types for Statefulsets." fixed in
  263. // https://github.com/kubernetes/kubernetes/blob/7f23a743e8c23ac6489340bbb34fa6f1d392db9d/pkg/apis/apps/types.go#L205
  264. }
  265. var specDesired int32
  266. if statefulSet.Spec.Replicas != nil {
  267. rollout.Desired = *statefulSet.Spec.Replicas
  268. specDesired = *statefulSet.Spec.Replicas
  269. }
  270. // rolling update
  271. if statefulSet.Spec.UpdateStrategy.Type == apiapps.RollingUpdateStatefulSetStrategyType &&
  272. statefulSet.Spec.UpdateStrategy.RollingUpdate != nil &&
  273. statefulSet.Spec.UpdateStrategy.RollingUpdate.Partition != nil {
  274. // Desired for this partition: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#partitions
  275. desiredPartition := rollout.Desired - *statefulSet.Spec.UpdateStrategy.RollingUpdate.Partition
  276. if desiredPartition >= 0 {
  277. rollout.Desired = desiredPartition
  278. } else {
  279. rollout.Desired = 0
  280. }
  281. }
  282. if statefulSetStatus.CurrentRevision != statefulSetStatus.UpdateRevision {
  283. // rollout in progress
  284. rollout.Updated = statefulSetStatus.UpdatedReplicas
  285. } else {
  286. // rollout complete
  287. rollout.Updated = statefulSetStatus.CurrentReplicas
  288. }
  289. rollout.Outdated = rollout.Desired - rollout.Updated
  290. if statefulSetStatus.ObservedGeneration >= objectMeta.Generation {
  291. // the definition has been updated; now let's see about the replicas
  292. status = cluster.StatusUpdating
  293. // for partition rolling update rollout.Ready might be >= rollout.Desired
  294. // because of rollout.Ready references to all ready pods (updated and outdated ones)
  295. // and rollout.Desired references to only desired pods for current partition
  296. // we check that all pods (updated and outdated ones) are ready
  297. if rollout.Updated == rollout.Desired && rollout.Ready == specDesired && rollout.Outdated == 0 {
  298. status = cluster.StatusReady
  299. }
  300. }
  301. // apiVersion & kind must be set, since TypeMeta is not populated
  302. statefulSet.APIVersion = "apps/v1"
  303. statefulSet.Kind = "StatefulSet"
  304. return workload{
  305. status: status,
  306. rollout: rollout,
  307. podTemplate: statefulSet.Spec.Template,
  308. k8sObject: statefulSet}
  309. }
  310. /////////////////////////////////////////////////////////////////////////////
  311. // batch/v1beta1 CronJob
  312. type cronJobKind struct{}
  313. func (dk *cronJobKind) getWorkload(ctx context.Context, c *Cluster, namespace, name string) (workload, error) {
  314. if err := ctx.Err(); err != nil {
  315. return workload{}, err
  316. }
  317. cronJob, err := c.client.BatchV1beta1().CronJobs(namespace).Get(name, meta_v1.GetOptions{})
  318. if err != nil {
  319. return workload{}, err
  320. }
  321. return makeCronJobWorkload(cronJob), nil
  322. }
  323. func (dk *cronJobKind) getWorkloads(ctx context.Context, c *Cluster, namespace string) ([]workload, error) {
  324. if err := ctx.Err(); err != nil {
  325. return nil, err
  326. }
  327. cronJobs, err := c.client.BatchV1beta1().CronJobs(namespace).List(meta_v1.ListOptions{})
  328. if err != nil {
  329. return nil, err
  330. }
  331. var workloads []workload
  332. for i, _ := range cronJobs.Items {
  333. workloads = append(workloads, makeCronJobWorkload(&cronJobs.Items[i]))
  334. }
  335. return workloads, nil
  336. }
  337. func makeCronJobWorkload(cronJob *apibatch.CronJob) workload {
  338. cronJob.APIVersion = "batch/v1beta1"
  339. cronJob.Kind = "CronJob"
  340. return workload{
  341. status: cluster.StatusReady,
  342. podTemplate: cronJob.Spec.JobTemplate.Spec.Template,
  343. k8sObject: cronJob}
  344. }
  345. /////////////////////////////////////////////////////////////////////////////
  346. // helm.integrations.flux.weave.works/v1alpha2 FluxHelmRelease
  347. type fluxHelmReleaseKind struct{}
  348. func (fhr *fluxHelmReleaseKind) getWorkload(ctx context.Context, c *Cluster, namespace, name string) (workload, error) {
  349. if err := ctx.Err(); err != nil {
  350. return workload{}, err
  351. }
  352. fluxHelmRelease, err := c.client.HelmV1alpha2().FluxHelmReleases(namespace).Get(name, meta_v1.GetOptions{})
  353. if err != nil {
  354. return workload{}, err
  355. }
  356. return makeFluxHelmReleaseWorkload(fluxHelmRelease), nil
  357. }
  358. func (fhr *fluxHelmReleaseKind) getWorkloads(ctx context.Context, c *Cluster, namespace string) ([]workload, error) {
  359. if err := ctx.Err(); err != nil {
  360. return nil, err
  361. }
  362. fluxHelmReleases, err := c.client.HelmV1alpha2().FluxHelmReleases(namespace).List(meta_v1.ListOptions{})
  363. if err != nil {
  364. return nil, err
  365. }
  366. var workloads []workload
  367. for i, _ := range fluxHelmReleases.Items {
  368. workloads = append(workloads, makeFluxHelmReleaseWorkload(&fluxHelmReleases.Items[i]))
  369. }
  370. return workloads, nil
  371. }
  372. func makeFluxHelmReleaseWorkload(fluxHelmRelease *fhr_v1alpha2.FluxHelmRelease) workload {
  373. containers := createK8sHRContainers(fluxHelmRelease.ObjectMeta.Annotations, fluxHelmRelease.Spec.Values)
  374. podTemplate := apiv1.PodTemplateSpec{
  375. ObjectMeta: fluxHelmRelease.ObjectMeta,
  376. Spec: apiv1.PodSpec{
  377. Containers: containers,
  378. ImagePullSecrets: []apiv1.LocalObjectReference{},
  379. },
  380. }
  381. // apiVersion & kind must be set, since TypeMeta is not populated
  382. fluxHelmRelease.APIVersion = "helm.integrations.flux.weave.works/v1alpha2"
  383. fluxHelmRelease.Kind = "FluxHelmRelease"
  384. return workload{
  385. status: fluxHelmRelease.Status.ReleaseStatus,
  386. podTemplate: podTemplate,
  387. k8sObject: fluxHelmRelease,
  388. }
  389. }
  390. // createK8sContainers creates a list of k8s containers by
  391. // interpreting the HelmRelease resource. The interpretation is
  392. // analogous to that in cluster/kubernetes/resource/fluxhelmrelease.go
  393. func createK8sHRContainers(annotations map[string]string, values map[string]interface{}) []apiv1.Container {
  394. var containers []apiv1.Container
  395. kresource.FindHelmReleaseContainers(annotations, values, func(name string, image image.Ref, _ kresource.ImageSetter) error {
  396. containers = append(containers, apiv1.Container{
  397. Name: name,
  398. Image: image.String(),
  399. })
  400. return nil
  401. })
  402. return containers
  403. }
  404. /////////////////////////////////////////////////////////////////////////////
  405. // flux.weave.works/v1beta1 HelmRelease
  406. // flux.fluxcd.io/v1 HelmRelease
  407. type helmReleaseKind struct{}
  408. // getWorkload attempts to resolve a HelmRelease, it does so by first
  409. // requesting the v1 version, and falling back to v1beta1 if this gives
  410. // no result. In case the latter also fails it returns the error.
  411. // TODO(hidde): this creates a new problem, as it will always return
  412. // the error for the v1beta1 resource. Which may not be accurate in
  413. // case v1beta1 is not active in the cluster at all. One potential
  414. // solution may be to collect both errors and see if one outweighs
  415. // the other.
  416. func (hr *helmReleaseKind) getWorkload(ctx context.Context, c *Cluster, namespace, name string) (workload, error) {
  417. if err := ctx.Err(); err != nil {
  418. return workload{}, err
  419. }
  420. if helmRelease, err := c.client.HelmV1().HelmReleases(namespace).Get(name, meta_v1.GetOptions{}); err == nil {
  421. return makeHelmReleaseStableWorkload(helmRelease), err
  422. }
  423. helmRelease, err := c.client.FluxV1beta1().HelmReleases(namespace).Get(name, meta_v1.GetOptions{})
  424. if err != nil {
  425. return workload{}, err
  426. }
  427. return makeHelmReleaseBetaWorkload(helmRelease), nil
  428. }
  429. // getWorkloads collects v1 and v1beta1 HelmRelease workloads, if the
  430. // same workload (by name) is found for two versions, only the v1
  431. // version is returned. This is so that the workload results returned
  432. // by this method are always valid for `getWorkload` and return the
  433. // same resource.
  434. // TODO(hidde): again, the cost of backwards compatibility is silencing
  435. // errors.
  436. func (hr *helmReleaseKind) getWorkloads(ctx context.Context, c *Cluster, namespace string) ([]workload, error) {
  437. if err := ctx.Err(); err != nil {
  438. return nil, err
  439. }
  440. names := make(map[string]bool, 0)
  441. workloads := make([]workload, 0)
  442. if helmReleases, err := c.client.HelmV1().HelmReleases(namespace).List(meta_v1.ListOptions{}); err == nil {
  443. for i, _ := range helmReleases.Items {
  444. workload := makeHelmReleaseStableWorkload(&helmReleases.Items[i])
  445. workloads = append(workloads, workload)
  446. names[workload.GetName()] = true
  447. }
  448. }
  449. if helmReleases, err := c.client.FluxV1beta1().HelmReleases(namespace).List(meta_v1.ListOptions{}); err == nil {
  450. for i, _ := range helmReleases.Items {
  451. workload := makeHelmReleaseBetaWorkload(&helmReleases.Items[i])
  452. if names[workload.GetName()] {
  453. continue
  454. }
  455. workloads = append(workloads, workload)
  456. }
  457. }
  458. return workloads, nil
  459. }
  460. func makeHelmReleaseBetaWorkload(helmRelease *hr_v1beta1.HelmRelease) workload {
  461. containers := createK8sHRContainers(helmRelease.ObjectMeta.Annotations, helmRelease.Spec.Values)
  462. podTemplate := apiv1.PodTemplateSpec{
  463. ObjectMeta: helmRelease.ObjectMeta,
  464. Spec: apiv1.PodSpec{
  465. Containers: containers,
  466. ImagePullSecrets: []apiv1.LocalObjectReference{},
  467. },
  468. }
  469. // apiVersion & kind must be set, since TypeMeta is not populated
  470. helmRelease.APIVersion = "flux.weave.works/v1beta1"
  471. helmRelease.Kind = "HelmRelease"
  472. return workload{
  473. status: helmRelease.Status.ReleaseStatus,
  474. podTemplate: podTemplate,
  475. k8sObject: helmRelease,
  476. }
  477. }
  478. func makeHelmReleaseStableWorkload(helmRelease *hr_v1.HelmRelease) workload {
  479. containers := createK8sHRContainers(helmRelease.ObjectMeta.Annotations, helmRelease.Spec.Values)
  480. podTemplate := apiv1.PodTemplateSpec{
  481. ObjectMeta: helmRelease.ObjectMeta,
  482. Spec: apiv1.PodSpec{
  483. Containers: containers,
  484. ImagePullSecrets: []apiv1.LocalObjectReference{},
  485. },
  486. }
  487. // apiVersion & kind must be set, since TypeMeta is not populated
  488. helmRelease.APIVersion = "helm.fluxcd.io/v1"
  489. helmRelease.Kind = "HelmRelease"
  490. return workload{
  491. status: helmRelease.Status.ReleaseStatus,
  492. podTemplate: podTemplate,
  493. k8sObject: helmRelease,
  494. }
  495. }