// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package k8s

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/blang/semver/v4"
	"helm.sh/helm/v4/pkg/action"
	"helm.sh/helm/v4/pkg/cli/output"
	appsv1 "k8s.io/api/apps/v1"
	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	discoveryv1 "k8s.io/api/discovery/v1"
	networkingv1 "k8s.io/api/networking/v1"
	rbacv1 "k8s.io/api/rbac/v1"
	apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
	apiextensionsclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/httpstream"
	"k8s.io/apimachinery/pkg/util/strategicpatch"
	"k8s.io/cli-runtime/pkg/genericclioptions"
	"k8s.io/cli-runtime/pkg/resource"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/kubernetes/scheme"
	_ "k8s.io/client-go/plugin/pkg/client/auth" // Register all auth providers (azure, gcp, oidc, openstack, ..）
	"k8s.io/client-go/rest"
	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
	"k8s.io/client-go/tools/portforward"
	"k8s.io/client-go/transport/spdy"

	"github.com/cilium/cilium/api/v1/models"
	"github.com/cilium/cilium/cilium-cli/defaults"
	ciliumv2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
	ciliumv2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
	ciliumClientset "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned"
	slimcorev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1"
	ciliumnetworkingv1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/networking/v1"
	slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/client/clientset/versioned/typed/core/v1"
	slim_networkingv1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/client/clientset/versioned/typed/networking/v1"
	"github.com/cilium/cilium/pkg/safeio"
	"github.com/cilium/cilium/pkg/versioncheck"
)

const getLogsRetries = 3

func init() {
	// Register the Cilium types in the default scheme.
	_ = ciliumv2.AddToScheme(scheme.Scheme)
	_ = ciliumv2alpha1.AddToScheme(scheme.Scheme)
}

type Client struct {
	Clientset                 kubernetes.Interface
	ExtensionClientset        apiextensionsclientset.Interface // k8s api extension needed to retrieve CRDs
	DynamicClientset          dynamic.Interface
	CiliumClientset           ciliumClientset.Interface
	SlimCoreV1Clientset       slim_corev1.CoreV1Interface
	SlimNetworkingV1Clientset slim_networkingv1.NetworkingV1Interface
	Config                    *rest.Config
	RawConfig                 clientcmdapi.Config
	RESTClientGetter          genericclioptions.RESTClientGetter
	contextName               string
	HelmActionConfig          *action.Configuration
}

func NewClient(contextName, kubeconfig, ciliumNamespace string, impersonateAs string, impersonateGroup []string) (*Client, error) {
	restClientGetter := genericclioptions.ConfigFlags{
		Context:    &contextName,
		KubeConfig: &kubeconfig,
	}
	rawKubeConfigLoader := restClientGetter.ToRawKubeConfigLoader()

	config, err := rawKubeConfigLoader.ClientConfig()
	if err != nil {
		return nil, err
	}

	if impersonateAs != "" || len(impersonateGroup) > 0 {
		config.Impersonate = rest.ImpersonationConfig{
			UserName: impersonateAs,
			Groups:   impersonateGroup,
		}
	}

	rawConfig, err := rawKubeConfigLoader.RawConfig()
	if err != nil {
		return nil, err
	}

	ciliumClientset, err := ciliumClientset.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	extensionClientset, err := apiextensionsclientset.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	dynamicClientset, err := dynamic.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	slimCoreV1Clientset, err := slim_corev1.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	slimNetworkingV1Clientset, err := slim_networkingv1.NewForConfig(config)
	if err != nil {
		return nil, err
	}

	if contextName == "" {
		contextName = rawConfig.CurrentContext
	}

	// Initialize Helm action configuration.
	// Use the default Helm driver (Kubernetes secret).
	helmDriver := ""
	actionConfig := action.Configuration{}
	if err := actionConfig.Init(&restClientGetter, ciliumNamespace, helmDriver); err != nil {
		return nil, err
	}

	return &Client{
		CiliumClientset:           ciliumClientset,
		Clientset:                 clientset,
		ExtensionClientset:        extensionClientset,
		SlimCoreV1Clientset:       slimCoreV1Clientset,
		SlimNetworkingV1Clientset: slimNetworkingV1Clientset,
		Config:                    config,
		DynamicClientset:          dynamicClientset,
		RawConfig:                 rawConfig,
		RESTClientGetter:          &restClientGetter,
		contextName:               contextName,
		HelmActionConfig:          &actionConfig,
	}, nil
}

// ContextName returns the name of the context the client is connected to
func (c *Client) ContextName() (name string) {
	return c.contextName
}

// ClusterName returns the name of the cluster the client is connected to
func (c *Client) ClusterName() (name string) {
	if context, ok := c.RawConfig.Contexts[c.ContextName()]; ok {
		name = context.Cluster
	}
	return
}

func (c *Client) GetAPIServerHostAndPort() (string, string) {
	if context, ok := c.RawConfig.Contexts[c.ContextName()]; ok {
		addr := c.RawConfig.Clusters[context.Cluster].Server
		if addr != "" {
			url, err := url.Parse(addr)
			if err == nil {
				host, port, _ := net.SplitHostPort(url.Host)
				return host, port
			}
		}
	}
	return "", ""
}

func (c *Client) CreateSecret(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.CreateOptions) (*corev1.Secret, error) {
	return c.Clientset.CoreV1().Secrets(namespace).Create(ctx, secret, opts)
}

func (c *Client) UpdateSecret(ctx context.Context, namespace string, secret *corev1.Secret, opts metav1.UpdateOptions) (*corev1.Secret, error) {
	return c.Clientset.CoreV1().Secrets(namespace).Update(ctx, secret, opts)
}

func (c *Client) PatchSecret(ctx context.Context, namespace, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions) (*corev1.Secret, error) {
	return c.Clientset.CoreV1().Secrets(namespace).Patch(ctx, name, pt, data, opts)
}

func (c *Client) DeleteSecret(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.Clientset.CoreV1().Secrets(namespace).Delete(ctx, name, opts)
}

func (c *Client) GetSecret(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*corev1.Secret, error) {
	return c.Clientset.CoreV1().Secrets(namespace).Get(ctx, name, opts)
}

func (c *Client) CreateServiceAccount(ctx context.Context, namespace string, account *corev1.ServiceAccount, opts metav1.CreateOptions) (*corev1.ServiceAccount, error) {
	return c.Clientset.CoreV1().ServiceAccounts(namespace).Create(ctx, account, opts)
}

func (c *Client) DeleteServiceAccount(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.Clientset.CoreV1().ServiceAccounts(namespace).Delete(ctx, name, opts)
}

func (c *Client) GetServiceAccount(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*corev1.ServiceAccount, error) {
	return c.Clientset.CoreV1().ServiceAccounts(namespace).Get(ctx, name, opts)
}

func (c *Client) GetClusterRole(ctx context.Context, name string, opts metav1.GetOptions) (*rbacv1.ClusterRole, error) {
	return c.Clientset.RbacV1().ClusterRoles().Get(ctx, name, opts)
}

func (c *Client) GetConfigMap(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*corev1.ConfigMap, error) {
	return c.Clientset.CoreV1().ConfigMaps(namespace).Get(ctx, name, opts)
}

func (c *Client) PatchConfigMap(ctx context.Context, namespace, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions) (*corev1.ConfigMap, error) {
	return c.Clientset.CoreV1().ConfigMaps(namespace).Patch(ctx, name, pt, data, opts)
}

func (c *Client) CreateService(ctx context.Context, namespace string, service *corev1.Service, opts metav1.CreateOptions) (*corev1.Service, error) {
	return c.Clientset.CoreV1().Services(namespace).Create(ctx, service, opts)
}

func (c *Client) DeleteService(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.Clientset.CoreV1().Services(namespace).Delete(ctx, name, opts)
}

func (c *Client) GetService(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*corev1.Service, error) {
	return c.Clientset.CoreV1().Services(namespace).Get(ctx, name, opts)
}

func (c *Client) GetEndpointSlice(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*discoveryv1.EndpointSlice, error) {
	return c.Clientset.DiscoveryV1().EndpointSlices(namespace).Get(ctx, name, opts)
}

func (c *Client) CreateDeployment(ctx context.Context, namespace string, deployment *appsv1.Deployment, opts metav1.CreateOptions) (*appsv1.Deployment, error) {
	return c.Clientset.AppsV1().Deployments(namespace).Create(ctx, deployment, opts)
}

func (c *Client) GetDeployment(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*appsv1.Deployment, error) {
	return c.Clientset.AppsV1().Deployments(namespace).Get(ctx, name, opts)
}

func (c *Client) ListDeployment(ctx context.Context, namespace string, options metav1.ListOptions) (*appsv1.DeploymentList, error) {
	return c.Clientset.AppsV1().Deployments(namespace).List(ctx, options)
}

func (c *Client) DeleteDeployment(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.Clientset.AppsV1().Deployments(namespace).Delete(ctx, name, opts)
}

func (c *Client) CheckDeploymentStatus(ctx context.Context, namespace, deployment string) error {
	d, err := c.GetDeployment(ctx, namespace, deployment, metav1.GetOptions{})
	if err != nil {
		return err
	}

	if d == nil {
		return fmt.Errorf("deployment is not available")
	}

	if d.Status.ObservedGeneration != d.Generation {
		return fmt.Errorf("observed generation (%d) is older than generation of the desired state (%d)",
			d.Status.ObservedGeneration, d.Generation)
	}

	if d.Status.Replicas == 0 {
		return fmt.Errorf("replicas count is zero")
	}

	if d.Status.AvailableReplicas != d.Status.Replicas {
		return fmt.Errorf("only %d of %d replicas are available", d.Status.AvailableReplicas, d.Status.Replicas)
	}

	if d.Status.ReadyReplicas != d.Status.Replicas {
		return fmt.Errorf("only %d of %d replicas are ready", d.Status.ReadyReplicas, d.Status.Replicas)
	}

	if d.Status.UpdatedReplicas != d.Status.Replicas {
		return fmt.Errorf("only %d of %d replicas are up-to-date", d.Status.UpdatedReplicas, d.Status.Replicas)
	}

	return nil
}

func (c *Client) CreateNamespace(ctx context.Context, namespace *corev1.Namespace, opts metav1.CreateOptions) (*corev1.Namespace, error) {
	return c.Clientset.CoreV1().Namespaces().Create(ctx, namespace, opts)
}

func (c *Client) UpdateNamespace(ctx context.Context, namespace *corev1.Namespace, opts metav1.UpdateOptions) (*corev1.Namespace, error) {
	return c.Clientset.CoreV1().Namespaces().Update(ctx, namespace, opts)
}

func (c *Client) GetNamespace(ctx context.Context, namespace string, options metav1.GetOptions) (*corev1.Namespace, error) {
	return c.Clientset.CoreV1().Namespaces().Get(ctx, namespace, options)
}

func (c *Client) DeleteNamespace(ctx context.Context, namespace string, opts metav1.DeleteOptions) error {
	return c.Clientset.CoreV1().Namespaces().Delete(ctx, namespace, opts)
}

func (c *Client) GetPod(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*corev1.Pod, error) {
	return c.Clientset.CoreV1().Pods(namespace).Get(ctx, name, opts)
}

func (c *Client) GetRaw(ctx context.Context, path string) (string, error) {
	result := c.Clientset.CoreV1().RESTClient().Get().AbsPath(path).Do(ctx)

	response, err := result.Raw()
	if err != nil {
		return "", err
	}
	return string(response), nil
}

func (c *Client) CreatePod(ctx context.Context, namespace string, pod *corev1.Pod, opts metav1.CreateOptions) (*corev1.Pod, error) {
	return c.Clientset.CoreV1().Pods(namespace).Create(ctx, pod, opts)
}

func (c *Client) DeletePod(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.Clientset.CoreV1().Pods(namespace).Delete(ctx, name, opts)
}

func (c *Client) DeletePodCollection(ctx context.Context, namespace string, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error {
	return c.Clientset.CoreV1().Pods(namespace).DeleteCollection(ctx, opts, listOpts)
}

func (c *Client) ListPods(ctx context.Context, namespace string, options metav1.ListOptions) (*corev1.PodList, error) {
	return c.Clientset.CoreV1().Pods(namespace).List(ctx, options)
}

func (c *Client) PodLogs(namespace, name string, opts *corev1.PodLogOptions) *rest.Request {
	return c.Clientset.CoreV1().Pods(namespace).GetLogs(name, opts)
}

func (c *Client) ContainerLogs(ctx context.Context, namespace, pod, containerName string, since time.Time, previous bool) (string, error) {
	opts := &corev1.PodLogOptions{
		Container:  containerName,
		Timestamps: true,
		SinceTime:  &metav1.Time{Time: since},
		Previous:   previous,
	}
	req := c.PodLogs(namespace, pod, opts)
	podLogs, err := req.Stream(ctx)
	if err != nil {
		return "", fmt.Errorf("error getting cilium-agent logs for %s/%s: %w", namespace, pod, err)
	}
	defer podLogs.Close()

	log, err := safeio.ReadAllLimit(podLogs, safeio.GB)
	if err != nil && !errors.Is(err, safeio.ErrLimitReached) {
		return "", fmt.Errorf("error reading log: %w", err)
	}

	return string(log), nil
}

func (c *Client) ListServices(ctx context.Context, namespace string, options metav1.ListOptions) (*corev1.ServiceList, error) {
	return c.Clientset.CoreV1().Services(namespace).List(ctx, options)
}

func (c *Client) ExecInPodWithStderr(ctx context.Context, namespace, pod, container string, command []string) (bytes.Buffer, bytes.Buffer, error) {
	var stdout, stderr bytes.Buffer
	err := c.ExecInPodWithWriters(ctx, nil, namespace, pod, container, command, &stdout, &stderr)
	return stdout, stderr, err
}

func (c *Client) ExecInPod(ctx context.Context, namespace, pod, container string, command []string) (bytes.Buffer, error) {
	var stdout bytes.Buffer
	err := c.ExecInPodWithWriters(ctx, nil, namespace, pod, container, command, &stdout, StderrAsError)
	return stdout, err
}

// StderrAsError, when given as stderr parameter of the ExecInPodWithWriters function,
// causes any stderr message to be returned as an error.
var StderrAsError stderrAsError

type stderrAsError struct{}

func (stderrAsError) Write([]byte) (int, error) { return 0, errors.ErrUnsupported }

func (c *Client) ExecInPodWithWriters(connCtx, killCmdCtx context.Context, namespace, pod, container string, command []string, stdout, stderr io.Writer) error {
	var errbuf *bytes.Buffer

	if _, ok := stderr.(stderrAsError); ok {
		errbuf = &bytes.Buffer{}
		stderr = errbuf
	}

	execParams := ExecParameters{
		Namespace: namespace,
		Pod:       pod,
		Container: container,
		Command:   command,
	}
	if killCmdCtx != nil {
		execParams.TTY = true
	}

	err := c.execInPodWithWriters(connCtx, killCmdCtx, execParams, stdout, stderr)

	var errstr string
	if errbuf != nil {
		errstr = errbuf.String()
	}

	switch {
	case err != nil && errstr == "":
		return fmt.Errorf("command failed (pod=%s/%s, container=%s): %w", namespace, pod, container, err)
	case err != nil && errstr != "":
		return fmt.Errorf("command failed (pod=%s/%s, container=%s): %w: %q", namespace, pod, container, err, errstr)
	case err == nil && errstr != "":
		return fmt.Errorf("command failed (pod=%s/%s, container=%s): %q", namespace, pod, container, errstr)
	default:
		return nil
	}
}

func (c *Client) CiliumStatus(ctx context.Context, namespace, pod string) (*models.StatusResponse, error) {
	stdout, err := c.ExecInPod(ctx, namespace, pod, defaults.AgentContainerName, []string{"cilium", "status", "-o", "json"})
	if err != nil {
		return nil, err
	}

	statusResponse := models.StatusResponse{}

	if err := json.Unmarshal(stdout.Bytes(), &statusResponse); err != nil {
		return nil, fmt.Errorf("unable to unmarshal response of cilium status: %w", err)
	}

	return &statusResponse, nil
}

// KVStoreMeshStatusNotImplemented is a sentinel error to signal that the status command is not implemented.
var ErrKVStoreMeshStatusNotImplemented = errors.New("kvstoremesh-dbg status is not available")

func (c *Client) KVStoreMeshStatus(ctx context.Context, namespace, pod string) ([]*models.RemoteCluster, error) {
	stdout, stderr, err := c.ExecInPodWithStderr(ctx, namespace, pod, defaults.ClusterMeshKVStoreMeshContainerName,
		[]string{defaults.ClusterMeshBinaryName, "kvstoremesh-dbg", "status", "-o", "json"})
	if err != nil {
		// Try to figure out if the status command is not yet supported in this version
		stderrStr := stderr.String()
		if strings.Contains(stderrStr, "Usage:") || strings.Contains(stderrStr, "unknown command") {
			return nil, ErrKVStoreMeshStatusNotImplemented
		}

		return nil, err
	}

	statusResponse := make([]*models.RemoteCluster, 0)
	if err := json.Unmarshal(stdout.Bytes(), &statusResponse); err != nil {
		return nil, fmt.Errorf("unable to unmarshal response of kvstoremesh-dbg status: %w", err)
	}

	return statusResponse, nil
}

func (c *Client) CiliumDbgEndpoints(ctx context.Context, namespace, pod string) ([]*models.Endpoint, error) {
	stdout, err := c.ExecInPod(ctx, namespace, pod, defaults.AgentContainerName, []string{"cilium", "endpoint", "list", "-o", "json"})
	if err != nil {
		return nil, err
	}

	var response []*models.Endpoint
	if err := json.Unmarshal(stdout.Bytes(), &response); err != nil {
		return nil, fmt.Errorf("unable to unmarshal response: %w", err)
	}
	return response, nil
}

func (c *Client) CreateConfigMap(ctx context.Context, namespace string, config *corev1.ConfigMap, opts metav1.CreateOptions) (*corev1.ConfigMap, error) {
	return c.Clientset.CoreV1().ConfigMaps(namespace).Create(ctx, config, opts)
}

func (c *Client) DeleteConfigMap(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.Clientset.CoreV1().ConfigMaps(namespace).Delete(ctx, name, opts)
}

func (c *Client) CreateDaemonSet(ctx context.Context, namespace string, ds *appsv1.DaemonSet, opts metav1.CreateOptions) (*appsv1.DaemonSet, error) {
	return c.Clientset.AppsV1().DaemonSets(namespace).Create(ctx, ds, opts)
}

func (c *Client) PatchDaemonSet(ctx context.Context, namespace, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions) (*appsv1.DaemonSet, error) {
	return c.Clientset.AppsV1().DaemonSets(namespace).Patch(ctx, name, pt, data, opts)
}

func (c *Client) GetDaemonSet(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*appsv1.DaemonSet, error) {
	return c.Clientset.AppsV1().DaemonSets(namespace).Get(ctx, name, opts)
}

func (c *Client) ListDaemonSet(ctx context.Context, namespace string, o metav1.ListOptions) (*appsv1.DaemonSetList, error) {
	return c.Clientset.AppsV1().DaemonSets(namespace).List(ctx, o)
}

func (c *Client) CheckDaemonSetStatus(ctx context.Context, namespace, deployment string) error {
	d, err := c.GetDaemonSet(ctx, namespace, deployment, metav1.GetOptions{})
	if err != nil {
		return err
	}

	if d == nil {
		return fmt.Errorf("deployment is not available")
	}

	if d.Status.ObservedGeneration != d.Generation {
		return fmt.Errorf("observed generation (%d) is older than generation of the desired state (%d)",
			d.Status.ObservedGeneration, d.Generation)
	}

	if d.Status.CurrentNumberScheduled != d.Status.DesiredNumberScheduled {
		return fmt.Errorf("only %d of %d replicas are scheduled", d.Status.CurrentNumberScheduled, d.Status.DesiredNumberScheduled)
	}

	if d.Status.NumberReady != d.Status.DesiredNumberScheduled {
		return fmt.Errorf("only %d of %d replicas are ready", d.Status.NumberReady, d.Status.DesiredNumberScheduled)
	}

	if d.Status.UpdatedNumberScheduled != d.Status.DesiredNumberScheduled {
		return fmt.Errorf("only %d of %d replicas are up-to-date", d.Status.UpdatedNumberScheduled, d.Status.DesiredNumberScheduled)
	}

	return nil
}

func (c *Client) GetStatefulSet(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*appsv1.StatefulSet, error) {
	return c.Clientset.AppsV1().StatefulSets(namespace).Get(ctx, name, opts)
}

func (c *Client) GetCronJob(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*batchv1.CronJob, error) {
	return c.Clientset.BatchV1().CronJobs(namespace).Get(ctx, name, opts)
}

func (c *Client) GetCRD(ctx context.Context, name string, opts metav1.GetOptions) (*apiextensions.CustomResourceDefinition, error) {
	return c.ExtensionClientset.ApiextensionsV1().CustomResourceDefinitions().Get(ctx, name, opts)
}

// Kubernetes Network Policies specific commands

func (c *Client) DeleteKubernetesNetworkPolicy(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.Clientset.NetworkingV1().NetworkPolicies(namespace).Delete(ctx, name, opts)
}

type Kind int

const (
	KindUnknown Kind = iota
	KindMinikube
	KindKind
	KindEKS
	KindGKE
	KindAKS
	KindMicrok8s
	KindRancherDesktop
	KindK3s
	KindOpenShift
)

func (k Kind) String() string {
	switch k {
	case KindUnknown:
		return "unknown"
	case KindMinikube:
		return "minikube"
	case KindKind:
		return "kind"
	case KindEKS:
		return "EKS"
	case KindGKE:
		return "GKE"
	case KindAKS:
		return "AKS"
	case KindMicrok8s:
		return "microk8s"
	case KindRancherDesktop:
		return "rancher-desktop"
	case KindK3s:
		return "K3s"
	case KindOpenShift:
		return "OpenShift"
	default:
		return "invalid"
	}
}

type Flavor struct {
	ClusterName string
	Kind        Kind
}

type Platform struct {
	OS   string
	Arch string
}

func (c *Client) AutodetectFlavor(ctx context.Context) Flavor {
	f := Flavor{
		ClusterName: c.ClusterName(),
	}

	if c.ClusterName() == "minikube" || c.ContextName() == "minikube" {
		f.Kind = KindMinikube
		return f
	}

	if strings.HasPrefix(c.ClusterName(), "microk8s-") || c.ContextName() == "microk8s" {
		f.Kind = KindMicrok8s
	}

	if c.ClusterName() == "rancher-desktop" || c.ContextName() == "rancher-desktop" {
		f.Kind = KindRancherDesktop
		return f
	}

	// When creating a cluster with kind create cluster --name foo,
	// the context and cluster name are kind-foo.
	if strings.HasPrefix(c.ClusterName(), "kind-") || strings.HasPrefix(c.ContextName(), "kind-") {
		f.Kind = KindKind
		return f
	}

	if strings.HasPrefix(c.ClusterName(), "gke_") {
		f.Kind = KindGKE
		return f
	}

	// When creating a cluster with eksctl create cluster --name foo,
	// the cluster name is foo.<region>.eksctl.io
	if strings.HasSuffix(c.ClusterName(), ".eksctl.io") {
		f.Kind = KindEKS
		return f
	}

	if context, ok := c.RawConfig.Contexts[c.ContextName()]; ok {
		if cluster, ok := c.RawConfig.Clusters[context.Cluster]; ok {
			if strings.HasSuffix(cluster.Server, "eks.amazonaws.com") {
				f.Kind = KindEKS
				return f
			} else if strings.HasSuffix(cluster.Server, "azmk8s.io:443") {
				f.Kind = KindAKS
				return f
			}
		}
	}

	nodeList, err := c.ListNodes(ctx, metav1.ListOptions{})
	if err != nil {
		return f
	}
	for _, node := range nodeList.Items {
		if _, ok := node.Labels["cloud.google.com/gke-nodepool"]; ok {
			f.Kind = KindGKE
			return f
		}
		if _, ok := node.Labels["cloud.google.com/gke-os-distribution"]; ok {
			f.Kind = KindGKE
			return f
		}

		isMaster := node.Labels["node-role.kubernetes.io/master"]
		if isMaster != "true" {
			continue
		}
		instanceType, ok := node.Labels[corev1.LabelInstanceTypeStable]
		if !ok {
			instanceType = node.Labels[corev1.LabelInstanceType]
		}
		// Assume k3s if the k8s master node runs k3s
		if instanceType == "k3s" {
			f.Kind = KindK3s
			return f
		}
	}

	apiList, err := c.GetServerGroups()
	if err == nil {
		apiGroups := apiList.Groups
		for i := range apiGroups {
			if apiGroups[i].Name == "route.openshift.io" {
				f.Kind = KindOpenShift
				return f
			}
		}
	}

	return f
}

func (c *Client) ListCiliumEndpoints(ctx context.Context, namespace string, options metav1.ListOptions) (*ciliumv2.CiliumEndpointList, error) {
	return c.CiliumClientset.CiliumV2().CiliumEndpoints(namespace).List(ctx, options)
}

func (c *Client) ListCiliumEndpointSlices(ctx context.Context, options metav1.ListOptions) (*ciliumv2alpha1.CiliumEndpointSliceList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumEndpointSlices().List(ctx, options)
}

func (c *Client) ListCiliumEnvoyConfigs(ctx context.Context, namespace string, options metav1.ListOptions) (*ciliumv2.CiliumEnvoyConfigList, error) {
	return c.CiliumClientset.CiliumV2().CiliumEnvoyConfigs(namespace).List(ctx, options)
}

func (c *Client) GetNode(ctx context.Context, name string, opts metav1.GetOptions) (*corev1.Node, error) {
	return c.Clientset.CoreV1().Nodes().Get(ctx, name, opts)
}

func (c *Client) ListNodes(ctx context.Context, options metav1.ListOptions) (*corev1.NodeList, error) {
	return c.Clientset.CoreV1().Nodes().List(ctx, options)
}

func (c *Client) ListSlimNodes(ctx context.Context, options metav1.ListOptions) (*slimcorev1.NodeList, error) {
	return c.SlimCoreV1Clientset.Nodes().List(ctx, options)
}

func (c *Client) PatchNode(ctx context.Context, nodeName string, pt types.PatchType, data []byte) (*corev1.Node, error) {
	return c.Clientset.CoreV1().Nodes().Patch(ctx, nodeName, pt, data, metav1.PatchOptions{})
}

func (c *Client) ListCiliumNetworkPolicies(ctx context.Context, namespace string, opts metav1.ListOptions) (*ciliumv2.CiliumNetworkPolicyList, error) {
	return c.CiliumClientset.CiliumV2().CiliumNetworkPolicies(namespace).List(ctx, opts)
}

func (c *Client) DeleteCiliumNetworkPolicy(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.CiliumClientset.CiliumV2().CiliumNetworkPolicies(namespace).Delete(ctx, name, opts)
}

func (c *Client) ListCiliumEgressGatewayPolicies(ctx context.Context, opts metav1.ListOptions) (*ciliumv2.CiliumEgressGatewayPolicyList, error) {
	return c.CiliumClientset.CiliumV2().CiliumEgressGatewayPolicies().List(ctx, opts)
}

func (c *Client) DeleteCiliumEgressGatewayPolicy(ctx context.Context, name string, opts metav1.DeleteOptions) error {
	return c.CiliumClientset.CiliumV2().CiliumEgressGatewayPolicies().Delete(ctx, name, opts)
}

func (c *Client) GetCiliumLocalRedirectPolicy(ctx context.Context, namespace, name string, opts metav1.GetOptions) (*ciliumv2.CiliumLocalRedirectPolicy, error) {
	return c.CiliumClientset.CiliumV2().CiliumLocalRedirectPolicies(namespace).Get(ctx, name, opts)
}

func (c *Client) DeleteCiliumLocalRedirectPolicy(ctx context.Context, namespace, name string, opts metav1.DeleteOptions) error {
	return c.CiliumClientset.CiliumV2().CiliumLocalRedirectPolicies(namespace).Delete(ctx, name, opts)
}

func (c *Client) ListCiliumBGPClusterConfigs(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumBGPClusterConfigList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumBGPClusterConfigs().List(ctx, opts)
}

func (c *Client) ListCiliumBGPPeerConfigs(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumBGPPeerConfigList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumBGPPeerConfigs().List(ctx, opts)
}

func (c *Client) ListCiliumBGPAdvertisements(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumBGPAdvertisementList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumBGPAdvertisements().List(ctx, opts)
}

func (c *Client) ListCiliumBGPNodeConfigs(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumBGPNodeConfigList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumBGPNodeConfigs().List(ctx, opts)
}

func (c *Client) ListCiliumBGPNodeConfigOverrides(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumBGPNodeConfigOverrideList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumBGPNodeConfigOverrides().List(ctx, opts)
}

func (c *Client) ListCiliumCIDRGroups(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumCIDRGroupList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumCIDRGroups().List(ctx, opts)
}

func (c *Client) ListCiliumClusterwideNetworkPolicies(ctx context.Context, opts metav1.ListOptions) (*ciliumv2.CiliumClusterwideNetworkPolicyList, error) {
	return c.CiliumClientset.CiliumV2().CiliumClusterwideNetworkPolicies().List(ctx, opts)
}

func (c *Client) ListCiliumClusterwideEnvoyConfigs(ctx context.Context, opts metav1.ListOptions) (*ciliumv2.CiliumClusterwideEnvoyConfigList, error) {
	return c.CiliumClientset.CiliumV2().CiliumClusterwideEnvoyConfigs().List(ctx, opts)
}

func (c *Client) DeleteCiliumClusterwideNetworkPolicy(ctx context.Context, name string, opts metav1.DeleteOptions) error {
	return c.CiliumClientset.CiliumV2().CiliumClusterwideNetworkPolicies().Delete(ctx, name, opts)
}

func (c *Client) GetVersion(_ context.Context) (string, error) {
	v, err := c.Clientset.Discovery().ServerVersion()
	if err != nil {
		return "", fmt.Errorf("failed to get Kubernetes version: %w", err)
	}
	return fmt.Sprintf("%#v", *v), nil
}

func (c *Client) ListEvents(ctx context.Context, o metav1.ListOptions) (*corev1.EventList, error) {
	return c.Clientset.CoreV1().Events(corev1.NamespaceAll).List(ctx, o)
}

func (c *Client) ListNamespaces(ctx context.Context, o metav1.ListOptions) (*corev1.NamespaceList, error) {
	return c.Clientset.CoreV1().Namespaces().List(ctx, o)
}

func (c *Client) GetPodsTable(_ context.Context) (*metav1.Table, error) {
	r := resource.NewBuilder(c.RESTClientGetter).
		Unstructured().
		AllNamespaces(true).
		ResourceTypes("pods").
		SingleResourceType().
		SelectAllParam(true).
		RequestChunksOf(500).
		ContinueOnError().
		Latest().
		Flatten().
		TransformRequests(func(r *rest.Request) {
			r.SetHeader(
				"Accept", fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName),
			)
		}).
		Do()
	if r.Err() != nil {
		return nil, r.Err()
	}
	infos, err := r.Infos()
	if err != nil {
		return nil, err
	}
	objects := make([]runtime.Object, 0, len(infos))
	for _, info := range infos {
		objects = append(objects, info.Object)
	}
	return unstructuredSliceToTable(objects)
}

func (c *Client) ProxyGet(ctx context.Context, namespace, name, url string) (string, error) {
	res := c.Clientset.CoreV1().RESTClient().Get().
		Namespace(namespace).
		Resource("pods").
		Name(name).
		SubResource("proxy").
		Suffix(url).
		Do(ctx)

	rawbody, err := res.Raw()
	if err != nil {
		return "", err
	}
	return string(rawbody), nil
}

func (c *Client) createDialer(url *url.URL) (httpstream.Dialer, error) {
	var errWebsocket, errSPDY error

	// We cannot control if errors from these constructors are due to lack of server support.
	// In the case of such errors, ignore them and later chose which dialer to return.
	dialerWebsocket, errWebsocket := portforward.NewSPDYOverWebsocketDialer(url, c.Config)

	transport, upgrader, errSPDY := spdy.RoundTripperFor(c.Config)
	dialerSPDY := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, url)

	// NewFallBackDialer returns a httpstream.Dialer which attempts a
	// connection with a primry dialer and a secondary dialer. However, it
	// does this by calling a method on both the primary and secondary
	// dialers. This means that both of them must not be nil if we want to
	// avoid a crash. Therefore, if either primary or secondary encountered
	// an error, return the other one.
	if errSPDY != nil && errWebsocket == nil {
		return dialerWebsocket, nil
	}
	if errWebsocket != nil && errSPDY == nil {
		return dialerSPDY, nil
	}

	if errSPDY != nil && errWebsocket != nil {
		return nil, fmt.Errorf("Error while creating k8s dialer: (websocket) %w, (spdy) %w", errWebsocket, errSPDY)
	}

	// Default to the SPDY connection
	dialerFallback := portforward.NewFallbackDialer(dialerSPDY, dialerWebsocket, func(err error) bool {
		return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)
	})
	return dialerFallback, nil
}

func (c *Client) ProxyTCP(ctx context.Context, namespace, name string, port uint16, handler func(io.ReadWriteCloser) error) error {
	request := c.Clientset.CoreV1().RESTClient().Post().
		Resource(corev1.ResourcePods.String()).
		Namespace(namespace).
		Name(name).
		SubResource("portforward")

	dialer, err := c.createDialer(request.URL())
	if err != nil {
		return err
	}

	const portForwardProtocolV1Name = "portforward.k8s.io"
	conn, proto, err := dialer.Dial(portForwardProtocolV1Name)
	if err != nil {
		return fmt.Errorf("connecting: %w", err)
	}

	defer conn.Close()
	if proto != portForwardProtocolV1Name {
		return fmt.Errorf("unable to negotiate protocol: client supports %q, server returned %q", portForwardProtocolV1Name, proto)
	}

	go func() {
		select {
		case <-ctx.Done():
			// Close aborts all remaining streams, and unblocks read operations.
			conn.Close()
		case <-conn.CloseChan():
		}
	}()

	return stream(conn, port, handler)
}

// The following is an adapted version of part of the client-go's port-forward connection handling implementation:
// https://github.com/kubernetes/client-go/blob/4ebe42d8c9c18f464fcc7b4f15b3a632db4cbdb2/tools/portforward/portforward.go#L335-L416
func stream(conn httpstream.Connection, port uint16, handler func(io.ReadWriteCloser) error) error {
	headers := http.Header{}
	headers.Set(corev1.StreamType, corev1.StreamTypeError)
	headers.Set(corev1.PortHeader, strconv.FormatUint(uint64(port), 10))

	errorStream, err := conn.CreateStream(headers)
	if err != nil {
		return fmt.Errorf("creating error stream: %w", err)
	}
	// we're not writing to this stream
	errorStream.Close()
	defer conn.RemoveStreams(errorStream)

	errorDone := make(chan error)
	go func() {
		defer close(errorDone)
		message, err := safeio.ReadAllLimit(errorStream, safeio.KB)
		switch {
		case err != nil:
			errorDone <- fmt.Errorf("reading from error stream: %w", err)
		case len(message) > 0:
			errorDone <- errors.New(string(message))
		}
	}()

	headers.Set(corev1.StreamType, corev1.StreamTypeData)
	dataStream, err := conn.CreateStream(headers)
	if err != nil {
		return fmt.Errorf("creating data stream: %w", err)
	}
	defer conn.RemoveStreams(dataStream)

	dataDone := make(chan error)
	go func() {
		defer close(dataDone)
		if err := handler(dataStream); err != nil {
			dataDone <- err
		}
	}()

	// Wait for both goroutines to terminate
	err = <-dataDone
	if err2 := <-errorDone; err2 != nil {
		err = err2
	}

	return err
}

func (c *Client) ListUnstructured(ctx context.Context, gvr schema.GroupVersionResource, namespace *string, o metav1.ListOptions) (*unstructured.UnstructuredList, error) {
	if namespace == nil {
		return c.DynamicClientset.Resource(gvr).List(ctx, o)
	}
	return c.DynamicClientset.Resource(gvr).Namespace(*namespace).List(ctx, o)
}

func (c *Client) ListEndpoints(ctx context.Context, o metav1.ListOptions) (*corev1.EndpointsList, error) {
	return c.Clientset.CoreV1().Endpoints(corev1.NamespaceAll).List(ctx, o)
}

func (c *Client) ListEndpointSlices(ctx context.Context, o metav1.ListOptions) (*discoveryv1.EndpointSliceList, error) {
	return c.Clientset.DiscoveryV1().EndpointSlices(corev1.NamespaceAll).List(ctx, o)
}

func (c *Client) ListIngressClasses(ctx context.Context, o metav1.ListOptions) (*networkingv1.IngressClassList, error) {
	return c.Clientset.NetworkingV1().IngressClasses().List(ctx, o)
}

func (c *Client) ListIngresses(ctx context.Context, o metav1.ListOptions) (*networkingv1.IngressList, error) {
	return c.Clientset.NetworkingV1().Ingresses(corev1.NamespaceAll).List(ctx, o)
}

func (c *Client) ListNetworkPolicies(ctx context.Context, o metav1.ListOptions) (*networkingv1.NetworkPolicyList, error) {
	return c.Clientset.NetworkingV1().NetworkPolicies(corev1.NamespaceAll).List(ctx, o)
}

func (c *Client) ListSlimNetworkPolicies(ctx context.Context, namespace string, o metav1.ListOptions) (*ciliumnetworkingv1.NetworkPolicyList, error) {
	return c.SlimNetworkingV1Clientset.NetworkPolicies(namespace).List(ctx, o)
}

func (c *Client) ListCiliumIdentities(ctx context.Context) (*ciliumv2.CiliumIdentityList, error) {
	return c.CiliumClientset.CiliumV2().CiliumIdentities().List(ctx, metav1.ListOptions{})
}

func (c *Client) ListCiliumNodes(ctx context.Context) (*ciliumv2.CiliumNodeList, error) {
	return c.CiliumClientset.CiliumV2().CiliumNodes().List(ctx, metav1.ListOptions{})
}

func (c *Client) ListCiliumNodeConfigs(ctx context.Context, namespace string, opts metav1.ListOptions) (*ciliumv2.CiliumNodeConfigList, error) {
	return c.CiliumClientset.CiliumV2().CiliumNodeConfigs(namespace).List(ctx, opts)
}

func (c *Client) ListCiliumPodIPPools(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumPodIPPoolList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumPodIPPools().List(ctx, opts)
}

func (c *Client) ListCiliumL2AnnouncementPolicies(ctx context.Context, opts metav1.ListOptions) (*ciliumv2alpha1.CiliumL2AnnouncementPolicyList, error) {
	return c.CiliumClientset.CiliumV2alpha1().CiliumL2AnnouncementPolicies().List(ctx, opts)
}

func (c *Client) GetLogs(ctx context.Context, namespace, name, container string, opts corev1.PodLogOptions, out io.Writer) error {
	opts.Container = container
	r := c.Clientset.CoreV1().Pods(namespace).GetLogs(name, &opts)
	var s io.ReadCloser
	var err error
	// retry request upon EOF to work around transient (?) failures on Azure, see
	// https://github.com/cilium/cilium/issues/29845
	for range getLogsRetries {
		s, err = r.Stream(ctx)
		if errors.Is(err, io.EOF) {
			continue
		}
		break
	}
	if err != nil {
		return err
	}
	defer s.Close()

	_, err = io.Copy(out, s)
	return err
}

// GetCiliumVersion returns a semver.Version representing the version of cilium
// running in the cilium-agent pod
func (c *Client) GetCiliumVersion(ctx context.Context, p *corev1.Pod) (*semver.Version, error) {
	o, _, err := c.ExecInPodWithStderr(
		ctx,
		p.Namespace,
		p.Name,
		defaults.AgentContainerName,
		[]string{"cilium", "version", "-o", "jsonpath={$.Daemon.Version}"},
	)
	if err != nil {
		return nil, fmt.Errorf("unable to fetch cilium version on pod %q: %w", p.Name, err)
	}

	v, _, _ := strings.Cut(strings.TrimSpace(o.String()), "-") // strips proprietary -releaseX suffix
	podVersion, err := semver.Parse(v)
	if err != nil {
		return nil, fmt.Errorf("unable to parse cilium version on pod %q: %w", p.Name, err)
	}

	return &podVersion, nil
}

func (c *Client) GetRunningCiliumVersion(ciliumHelmReleaseName string) (string, error) {
	m, err := action.NewGetMetadata(c.HelmActionConfig).Run(ciliumHelmReleaseName)
	if err != nil {
		return "", err
	}
	return m.Version, nil
}

func (c *Client) ListCiliumLocalRedirectPolicies(ctx context.Context, namespace string, opts metav1.ListOptions) (*ciliumv2.CiliumLocalRedirectPolicyList, error) {
	return c.CiliumClientset.CiliumV2().CiliumLocalRedirectPolicies(namespace).List(ctx, opts)
}

func (c *Client) GetServerVersion() (*semver.Version, error) {
	sv, err := c.Clientset.Discovery().ServerVersion()
	if err != nil {
		return nil, err
	}

	var ver semver.Version
	// Try GitVersion first. In case of error fallback to MajorMinor
	if sv.GitVersion != "" {
		// This is a string like "v1.9.0"
		ver, err = versioncheck.Version(sv.GitVersion)
		if err == nil {
			return &ver, nil
		}
	}

	if sv.Major != "" && sv.Minor != "" {
		ver, err = versioncheck.Version(fmt.Sprintf("%s.%s", sv.Major, sv.Minor))
		if err == nil {
			return &ver, nil
		}
	}
	return nil, fmt.Errorf("unable to parse Kubernetes version (got %s): %w", sv.String(), err)
}

func (c *Client) GetIngress(ctx context.Context, namespace string, name string, opts metav1.GetOptions) (*networkingv1.Ingress, error) {
	return c.Clientset.NetworkingV1().Ingresses(namespace).Get(ctx, name, opts)
}

func (c *Client) CreateIngress(ctx context.Context, namespace string, ingress *networkingv1.Ingress, opts metav1.CreateOptions) (*networkingv1.Ingress, error) {
	return c.Clientset.NetworkingV1().Ingresses(namespace).Create(ctx, ingress, opts)
}

// GetHelmValues is the function for cilium cli sysdump to collect the helm values from the release directly
func (c *Client) GetHelmValues(_ context.Context, releaseName string, namespace string) (string, error) {
	if c.RESTClientGetter == nil {
		return "", fmt.Errorf("no RESTClientGetter for Helm Values")
	}
	helmDriver := ""
	actionConfig := action.Configuration{}
	if err := actionConfig.Init(c.RESTClientGetter, namespace, helmDriver); err != nil {
		return "", err
	}
	helmGetValsClient := action.NewGetValues(&actionConfig)
	vals, err := helmGetValsClient.Run(releaseName)
	if err != nil {
		return "", fmt.Errorf("unable to retrieve helm value from release %s: %w", releaseName, err)
	}

	valuesBuf := new(bytes.Buffer)
	if err = output.EncodeYAML(valuesBuf, vals); err != nil {
		return "", fmt.Errorf("unable to parse helm values from release %s: %w", releaseName, err)
	}
	return valuesBuf.String(), nil
}

// GetHelmMetadata is the function for cilium cli sysdump to collect the helm metadata from the release directly
func (c *Client) GetHelmMetadata(_ context.Context, releaseName string, namespace string) (string, error) {
	if c.RESTClientGetter == nil {
		return "", fmt.Errorf("no RESTClientGetter for Helm Values")
	}
	helmDriver := ""
	actionConfig := action.Configuration{}
	if err := actionConfig.Init(c.RESTClientGetter, namespace, helmDriver); err != nil {
		return "", err
	}

	client := action.NewGetMetadata(&actionConfig)
	meta, err := client.Run(releaseName)
	if err != nil {
		return "", fmt.Errorf("unable to retrieve helm meta from release %s: %w", releaseName, err)
	}

	buf := new(bytes.Buffer)
	if err = output.EncodeYAML(buf, meta); err != nil {
		return "", fmt.Errorf("unable to parse helm metas from release %s: %w", releaseName, err)
	}
	return buf.String(), nil
}

// CreateEphemeralContainer will create a EphemeralContainer (debug container) in the specified pod.
// EphemeralContainers are special containers which can be added after-the-fact in running pods. They're
// useful for debugging, either when the target container image doesn't have necessary tools, or because
// the pod has no running containers due to a crash.
//
// see https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/
//
// EphemeralContainers were added in there current form (behind a feature gate) in 1.22. They are scheduled for GA in v1.25.
func (c *Client) CreateEphemeralContainer(ctx context.Context, pod *corev1.Pod, ec *corev1.EphemeralContainer) (*corev1.Pod, error) {
	oldJS, err := json.Marshal(pod)
	if err != nil {
		return nil, err // unlikely
	}

	newPod := pod.DeepCopy()
	newPod.Spec.EphemeralContainers = append(newPod.Spec.EphemeralContainers, *ec)

	newJS, err := json.Marshal(newPod)
	if err != nil {
		return nil, err // unlikely
	}

	patch, err := strategicpatch.CreateTwoWayMergePatch(oldJS, newJS, pod)
	if err != nil {
		return nil, fmt.Errorf("could not create patch: %w", err) // unlikely
	}

	return c.Clientset.CoreV1().Pods(pod.Namespace).Patch(
		ctx, pod.Name, types.StrategicMergePatchType, patch, metav1.PatchOptions{}, "ephemeralcontainers",
	)
}

type Object interface {
	metav1.Object
	runtime.Object
}

// ApplyGeneric uses server-side apply to merge changes to an arbitrary object.
// Returns the applied object.
func (c *Client) ApplyGeneric(ctx context.Context, obj Object) (*unstructured.Unstructured, error) {
	gvk, resource, err := c.Describe(obj)
	if err != nil {
		return nil, fmt.Errorf("could not get Kubernetes API information for %s/%s: %w", obj.GetNamespace(), obj.GetName(), err)
	}

	// Now, convert the object to an Unstructured
	u, ok := obj.(*unstructured.Unstructured)
	if !ok {
		b, err := json.Marshal(obj)
		if err != nil {
			return nil, fmt.Errorf("failed to convert to unstructured (marshal): %w", err)
		}
		u = &unstructured.Unstructured{}
		if err := json.Unmarshal(b, u); err != nil {
			return nil, fmt.Errorf("failed to convert to unstructured (unmarshal): %w", err)
		}
	}

	// Dragons: If we're passed a non-Unstructured object (e.g. v1.ConfigMap), it won't have
	// the GVK set necessarily. So, use the retrieved GVK from the schema and add it.
	// This is a no-op for Unstructured objects.
	// TODO: use a proper codec + serializer
	u.GetObjectKind().SetGroupVersionKind(gvk)

	// clear ManagedFields; it is not allowed to specify them in a Patch
	u.SetManagedFields(nil)

	dynamicClient := c.DynamicClientset.Resource(resource).Namespace(obj.GetNamespace())
	return dynamicClient.Apply(ctx, obj.GetName(), u, metav1.ApplyOptions{Force: true, FieldManager: "cilium-cli"})
}

func (c *Client) GetGeneric(ctx context.Context, namespace, name string, obj Object) (*unstructured.Unstructured, error) {
	_, resource, err := c.Describe(obj)
	if err != nil {
		return nil, fmt.Errorf("could not get Kubernetes API information for %s/%s: %w", obj.GetNamespace(), obj.GetName(), err)
	}

	dynamicClient := c.DynamicClientset.Resource(resource).Namespace(namespace)

	return dynamicClient.Get(ctx, name, metav1.GetOptions{})
}

func (c *Client) DeleteGeneric(ctx context.Context, obj Object) error {
	_, resource, err := c.Describe(obj)
	if err != nil {
		return fmt.Errorf("could not get Kubernetes API information for %s/%s: %w", obj.GetNamespace(), obj.GetName(), err)
	}

	dynamicClient := c.DynamicClientset.Resource(resource).Namespace(obj.GetNamespace())

	return dynamicClient.Delete(ctx, obj.GetName(), metav1.DeleteOptions{})
}

func (c *Client) GetServerGroups() (*metav1.APIGroupList, error) {
	return c.Clientset.Discovery().ServerGroups()
}

// OpenshiftClusterVersion represents the OpenShift ClusterVersion resource
type OpenshiftClusterVersion struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`
	Status            ClusterVersionStatus `json:"status,omitempty"`
}

// ClusterVersionStatus represents the status of the OpenShift ClusterVersion
type ClusterVersionStatus struct {
	Desired ClusterVersionUpdate `json:"desired,omitempty"`
}

// ClusterVersionUpdate represents the desired version information
type ClusterVersionUpdate struct {
	Version string `json:"version,omitempty"`
}

// GetOpenshiftClusterVersion retrieves the OpenShift ClusterVersion resource
func (c *Client) GetOpenshiftClusterVersion(ctx context.Context) (*OpenshiftClusterVersion, error) {
	gvr := schema.GroupVersionResource{
		Group:    "config.openshift.io",
		Version:  "v1",
		Resource: "clusterversions",
	}

	unstructuredObj, err := c.DynamicClientset.Resource(gvr).Get(ctx, "version", metav1.GetOptions{})
	if err != nil {
		return nil, fmt.Errorf("failed to get ClusterVersion: %w", err)
	}

	var cv OpenshiftClusterVersion
	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructuredObj.UnstructuredContent(), &cv); err != nil {
		return nil, fmt.Errorf("failed to convert ClusterVersion: %w", err)
	}

	return &cv, nil
}
