util.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. /*
  2. Copyright © 2025 ESO Maintainer Team
  3. Licensed under the Apache License, Version 2.0 (the "License");
  4. you may not use this file except in compliance with the License.
  5. You may obtain a copy of the License at
  6. https://www.apache.org/licenses/LICENSE-2.0
  7. Unless required by applicable law or agreed to in writing, software
  8. distributed under the License is distributed on an "AS IS" BASIS,
  9. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. See the License for the specific language governing permissions and
  11. limitations under the License.
  12. */
  13. package util
  14. import (
  15. "bytes"
  16. "context"
  17. "fmt"
  18. "net/http"
  19. "os"
  20. "path/filepath"
  21. "time"
  22. fluxhelm "github.com/fluxcd/helm-controller/api/v2"
  23. fluxsrc "github.com/fluxcd/source-controller/api/v1"
  24. v1 "k8s.io/api/core/v1"
  25. apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
  26. apierrors "k8s.io/apimachinery/pkg/api/errors"
  27. metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  28. "k8s.io/apimachinery/pkg/runtime"
  29. utilruntime "k8s.io/apimachinery/pkg/util/runtime"
  30. "k8s.io/apimachinery/pkg/util/wait"
  31. "k8s.io/client-go/kubernetes"
  32. clientgoscheme "k8s.io/client-go/kubernetes/scheme"
  33. "k8s.io/client-go/rest"
  34. restclient "k8s.io/client-go/rest"
  35. "k8s.io/client-go/tools/clientcmd"
  36. "k8s.io/client-go/tools/remotecommand"
  37. "k8s.io/client-go/util/homedir"
  38. crclient "sigs.k8s.io/controller-runtime/pkg/client"
  39. // nolint
  40. . "github.com/onsi/ginkgo/v2"
  41. esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
  42. esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1"
  43. genv1alpha1 "github.com/external-secrets/external-secrets/apis/generators/v1alpha1"
  44. awsv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/aws/v2alpha1"
  45. fakev2alpha1 "github.com/external-secrets/external-secrets/apis/provider/fake/v2alpha1"
  46. k8sv2alpha1 "github.com/external-secrets/external-secrets/apis/provider/kubernetes/v2alpha1"
  47. )
  48. var scheme = runtime.NewScheme()
  49. func init() {
  50. // kubernetes schemes
  51. utilruntime.Must(clientgoscheme.AddToScheme(scheme))
  52. utilruntime.Must(apiextensionsv1.AddToScheme(scheme))
  53. // external-secrets schemes
  54. utilruntime.Must(esv1.AddToScheme(scheme))
  55. utilruntime.Must(esv1alpha1.AddToScheme(scheme))
  56. utilruntime.Must(genv1alpha1.AddToScheme(scheme))
  57. // other schemes
  58. utilruntime.Must(fluxhelm.AddToScheme(scheme))
  59. utilruntime.Must(fluxsrc.AddToScheme(scheme))
  60. // v2alpha1 provider schemes
  61. utilruntime.Must(awsv2alpha1.AddToScheme(scheme))
  62. utilruntime.Must(fakev2alpha1.AddToScheme(scheme))
  63. utilruntime.Must(k8sv2alpha1.AddToScheme(scheme))
  64. }
  65. const (
  66. // How often to poll for conditions.
  67. Poll = 2 * time.Second
  68. )
  69. // CreateKubeNamespace creates a new Kubernetes Namespace for a test.
  70. func CreateKubeNamespace(baseName string, kubeClientSet kubernetes.Interface) (*v1.Namespace, error) {
  71. ns := &v1.Namespace{
  72. ObjectMeta: metav1.ObjectMeta{
  73. GenerateName: fmt.Sprintf("e2e-tests-%v-", baseName),
  74. },
  75. }
  76. return kubeClientSet.CoreV1().Namespaces().Create(GinkgoT().Context(), ns, metav1.CreateOptions{})
  77. }
  78. // DeleteKubeNamespace will delete a namespace resource.
  79. func DeleteKubeNamespace(namespace string, kubeClientSet kubernetes.Interface) error {
  80. return kubeClientSet.CoreV1().Namespaces().Delete(GinkgoT().Context(), namespace, metav1.DeleteOptions{})
  81. }
  82. // WaitForKubeNamespaceNotExist will wait for the namespace with the given name
  83. // to not exist for up to 2 minutes.
  84. func WaitForKubeNamespaceNotExist(namespace string, kubeClientSet kubernetes.Interface) error {
  85. return wait.PollUntilContextTimeout(GinkgoT().Context(), Poll, time.Minute*2, true, namespaceNotExist(kubeClientSet, namespace))
  86. }
  87. func namespaceNotExist(c kubernetes.Interface, namespace string) wait.ConditionWithContextFunc {
  88. return func(ctx context.Context) (bool, error) {
  89. _, err := c.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{})
  90. if apierrors.IsNotFound(err) {
  91. return true, nil
  92. }
  93. if err != nil {
  94. return false, err
  95. }
  96. return false, nil
  97. }
  98. }
  99. // ExecCmd exec command on specific pod and wait the command's output.
  100. func ExecCmd(client kubernetes.Interface, config *restclient.Config, podName, namespace string,
  101. command string) (string, error) {
  102. return execCmd(client, config, podName, "", namespace, command)
  103. }
  104. // ExecCmdWithContainer exec command on specific container in a specific pod and wait the command's output.
  105. func ExecCmdWithContainer(client kubernetes.Interface, config *restclient.Config, podName, containerName, namespace string,
  106. command string) (string, error) {
  107. return execCmd(client, config, podName, containerName, namespace, command)
  108. }
  109. func execCmd(client kubernetes.Interface, config *restclient.Config, podName, containerName, namespace string,
  110. command string) (string, error) {
  111. cmd := []string{
  112. "sh",
  113. "-c",
  114. command,
  115. }
  116. req := client.CoreV1().RESTClient().Post().Resource("pods").Name(podName).
  117. Namespace(namespace).SubResource("exec")
  118. option := &v1.PodExecOptions{
  119. Command: cmd,
  120. Container: containerName,
  121. Stdin: false,
  122. Stdout: true,
  123. Stderr: true,
  124. TTY: false,
  125. }
  126. req.VersionedParams(
  127. option,
  128. clientgoscheme.ParameterCodec,
  129. )
  130. exec, err := remotecommand.NewSPDYExecutor(config, "POST", req.URL())
  131. if err != nil {
  132. return "", err
  133. }
  134. var stdout, stderr bytes.Buffer
  135. err = exec.Stream(remotecommand.StreamOptions{
  136. Stdout: &stdout,
  137. Stderr: &stderr,
  138. Tty: false,
  139. })
  140. if err != nil {
  141. return "", fmt.Errorf("unable to exec stream: %w: \n%s\n%s", err, stdout.String(), stderr.String())
  142. }
  143. return stdout.String() + stderr.String(), nil
  144. }
  145. // WaitForPodsRunning waits for a given amount of time until a group of Pods is running in the given namespace.
  146. func WaitForPodsRunning(kubeClientSet kubernetes.Interface, expectedReplicas int, namespace string, opts metav1.ListOptions) (*v1.PodList, error) {
  147. var pods *v1.PodList
  148. err := wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, time.Minute*5, true, func(ctx context.Context) (bool, error) {
  149. pl, err := kubeClientSet.CoreV1().Pods(namespace).List(ctx, opts)
  150. if err != nil {
  151. return false, nil
  152. }
  153. r := 0
  154. for i := range pl.Items {
  155. if pl.Items[i].Status.Phase == v1.PodRunning {
  156. r++
  157. }
  158. }
  159. if r == expectedReplicas {
  160. pods = pl
  161. return true, nil
  162. }
  163. return false, nil
  164. })
  165. return pods, err
  166. }
  167. // WaitForPodsReady waits for a given amount of time until a group of Pods is running in the given namespace.
  168. func WaitForPodsReady(kubeClientSet kubernetes.Interface, expectedReplicas int, namespace string, opts metav1.ListOptions) error {
  169. return wait.PollUntilContextTimeout(GinkgoT().Context(), 1*time.Second, time.Minute*5, true, func(ctx context.Context) (bool, error) {
  170. pl, err := kubeClientSet.CoreV1().Pods(namespace).List(ctx, opts)
  171. if err != nil {
  172. return false, nil
  173. }
  174. r := 0
  175. for i := range pl.Items {
  176. if isRunning, _ := podRunningReady(&pl.Items[i]); isRunning {
  177. r++
  178. }
  179. }
  180. if r == expectedReplicas {
  181. return true, nil
  182. }
  183. return false, nil
  184. })
  185. }
  186. // podRunningReady checks whether pod p's phase is running and it has a ready
  187. // condition of status true.
  188. func podRunningReady(p *v1.Pod) (bool, error) {
  189. // Check the phase is running.
  190. if p.Status.Phase != v1.PodRunning {
  191. return false, fmt.Errorf("want pod '%s' on '%s' to be '%v' but was '%v'",
  192. p.ObjectMeta.Name, p.Spec.NodeName, v1.PodRunning, p.Status.Phase)
  193. }
  194. // Check the ready condition is true.
  195. if !isPodReady(p) {
  196. return false, fmt.Errorf("pod '%s' on '%s' didn't have condition {%v %v}; conditions: %v",
  197. p.ObjectMeta.Name, p.Spec.NodeName, v1.PodReady, v1.ConditionTrue, p.Status.Conditions)
  198. }
  199. return true, nil
  200. }
  201. func isPodReady(p *v1.Pod) bool {
  202. for _, condition := range p.Status.Conditions {
  203. if condition.Type != v1.ContainersReady {
  204. continue
  205. }
  206. return condition.Status == v1.ConditionTrue
  207. }
  208. return false
  209. }
  210. // WaitForURL tests the provided url. Once a http 200 is returned the func returns with no error.
  211. // Timeout is 5min.
  212. func WaitForURL(url string) error {
  213. return wait.PollUntilContextTimeout(GinkgoT().Context(), 2*time.Second, time.Minute*5, true, func(ctx context.Context) (bool, error) {
  214. req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
  215. if err != nil {
  216. return false, nil
  217. }
  218. res, err := http.DefaultClient.Do(req)
  219. if err != nil {
  220. return false, nil
  221. }
  222. defer func() {
  223. _ = res.Body.Close()
  224. }()
  225. if res.StatusCode == http.StatusOK {
  226. return true, nil
  227. }
  228. return false, err
  229. })
  230. }
  231. // UpdateKubeSA updates a new Kubernetes Service Account for a test.
  232. func UpdateKubeSA(baseName string, kubeClientSet kubernetes.Interface, ns string, annotations map[string]string) (*v1.ServiceAccount, error) {
  233. sa := &v1.ServiceAccount{
  234. ObjectMeta: metav1.ObjectMeta{
  235. Name: baseName,
  236. Annotations: annotations,
  237. },
  238. }
  239. return kubeClientSet.CoreV1().ServiceAccounts(ns).Update(GinkgoT().Context(), sa, metav1.UpdateOptions{})
  240. }
  241. // UpdateKubeSA updates a new Kubernetes Service Account for a test.
  242. func GetKubeSA(baseName string, kubeClientSet kubernetes.Interface, ns string) (*v1.ServiceAccount, error) {
  243. return kubeClientSet.CoreV1().ServiceAccounts(ns).Get(GinkgoT().Context(), baseName, metav1.GetOptions{})
  244. }
  245. func GetKubeSecret(client kubernetes.Interface, namespace, secretName string) (*v1.Secret, error) {
  246. return client.CoreV1().Secrets(namespace).Get(GinkgoT().Context(), secretName, metav1.GetOptions{})
  247. }
  248. // NewConfig loads and returns the kubernetes credentials from the environment.
  249. // KUBECONFIG env var takes precedence, then ~/.kube/config, then in-cluster config.
  250. func NewConfig() (*restclient.Config, *kubernetes.Clientset, crclient.Client) {
  251. var kubeConfig *restclient.Config
  252. var err error
  253. kcPath := os.Getenv("KUBECONFIG")
  254. if kcPath != "" {
  255. kubeConfig, err = clientcmd.BuildConfigFromFlags("", kcPath)
  256. if err != nil {
  257. Fail(err.Error())
  258. }
  259. } else {
  260. // Try ~/.kube/config
  261. homeDir, err := os.UserHomeDir()
  262. if err == nil {
  263. defaultKubeconfig := homeDir + "/.kube/config"
  264. if _, err := os.Stat(defaultKubeconfig); err == nil {
  265. kubeConfig, err = clientcmd.BuildConfigFromFlags("", defaultKubeconfig)
  266. if err != nil {
  267. Fail(err.Error())
  268. }
  269. }
  270. }
  271. // Fall back to in-cluster config if ~/.kube/config doesn't exist
  272. if kubeConfig == nil {
  273. kubeConfig, err = restclient.InClusterConfig()
  274. if err != nil {
  275. Fail(err.Error())
  276. }
  277. }
  278. }
  279. kubeClientSet, err := kubernetes.NewForConfig(kubeConfig)
  280. if err != nil {
  281. Fail(err.Error())
  282. }
  283. CRClient, err := crclient.New(kubeConfig, crclient.Options{Scheme: scheme})
  284. if err != nil {
  285. Fail(err.Error())
  286. }
  287. return kubeConfig, kubeClientSet, CRClient
  288. }
  289. func BuildKubeConfig() (*rest.Config, error) {
  290. // 1. If KUBECONFIG is explicitly set, use it
  291. if kubeconfigEnv := os.Getenv("KUBECONFIG"); kubeconfigEnv != "" {
  292. cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigEnv)
  293. if err == nil {
  294. return cfg, nil
  295. }
  296. return nil, fmt.Errorf("failed to load KUBECONFIG=%s: %w", kubeconfigEnv, err)
  297. }
  298. // 2. Try default kubeconfig location (~/.kube/config)
  299. if home := homedir.HomeDir(); home != "" {
  300. kubeconfigPath := filepath.Join(home, ".kube", "config")
  301. if _, err := os.Stat(kubeconfigPath); err == nil {
  302. cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
  303. if err == nil {
  304. return cfg, nil
  305. }
  306. return nil, fmt.Errorf("failed to load default kubeconfig: %w", err)
  307. }
  308. }
  309. // 3. Fallback to in-cluster config
  310. cfg, err := rest.InClusterConfig()
  311. if err != nil {
  312. return nil, fmt.Errorf("failed to load in-cluster config: %w", err)
  313. }
  314. return cfg, nil
  315. }