github.com/Racer159/jackal@v0.32.7-0.20240401174413-0bd2339e4f2e/src/pkg/cluster/data.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // SPDX-FileCopyrightText: 2021-Present The Jackal Authors
     3  
     4  // Package cluster contains Jackal-specific cluster management functions.
     5  package cluster
     6  
     7  import (
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  
    15  	"github.com/Racer159/jackal/src/config"
    16  	"github.com/Racer159/jackal/src/pkg/k8s"
    17  	"github.com/Racer159/jackal/src/pkg/layout"
    18  	"github.com/Racer159/jackal/src/pkg/message"
    19  	"github.com/Racer159/jackal/src/pkg/utils"
    20  	"github.com/Racer159/jackal/src/pkg/utils/exec"
    21  	"github.com/Racer159/jackal/src/types"
    22  	"github.com/defenseunicorns/pkg/helpers"
    23  	corev1 "k8s.io/api/core/v1"
    24  )
    25  
    26  // HandleDataInjection waits for the target pod(s) to come up and inject the data into them
    27  // todo:  this currently requires kubectl but we should have enough k8s work to make this native now.
    28  func (c *Cluster) HandleDataInjection(wg *sync.WaitGroup, data types.JackalDataInjection, componentPath *layout.ComponentPaths, dataIdx int) {
    29  	defer wg.Done()
    30  
    31  	injectionCompletionMarker := filepath.Join(componentPath.DataInjections, config.GetDataInjectionMarker())
    32  	if err := os.WriteFile(injectionCompletionMarker, []byte("🦄"), helpers.ReadWriteUser); err != nil {
    33  		message.WarnErrf(err, "Unable to create the data injection completion marker")
    34  		return
    35  	}
    36  
    37  	tarCompressFlag := ""
    38  	if data.Compress {
    39  		tarCompressFlag = "-z"
    40  	}
    41  
    42  	// Pod filter to ensure we only use the current deployment's pods
    43  	podFilterByInitContainer := func(pod corev1.Pod) bool {
    44  		// Look everywhere in the pod for a matching data injection marker
    45  		return strings.Contains(message.JSONValue(pod), config.GetDataInjectionMarker())
    46  	}
    47  
    48  	// Get the OS shell to execute commands in
    49  	shell, shellArgs := exec.GetOSShell(exec.Shell{Windows: "cmd"})
    50  
    51  	if _, _, err := exec.Cmd(shell, append(shellArgs, "tar --version")...); err != nil {
    52  		message.WarnErr(err, "Unable to execute tar on this system.  Please ensure it is installed and on your $PATH.")
    53  		return
    54  	}
    55  
    56  iterator:
    57  	// The eternal loop because some data injections can take a very long time
    58  	for {
    59  		message.Debugf("Attempting to inject data into %s", data.Target)
    60  		source := filepath.Join(componentPath.DataInjections, filepath.Base(data.Target.Path))
    61  		if helpers.InvalidPath(source) {
    62  			// The path is likely invalid because of how we compose OCI components, add an index suffix to the filename
    63  			source = filepath.Join(componentPath.DataInjections, strconv.Itoa(dataIdx), filepath.Base(data.Target.Path))
    64  			if helpers.InvalidPath(source) {
    65  				message.Warnf("Unable to find the data injection source path %s", source)
    66  				return
    67  			}
    68  		}
    69  
    70  		target := k8s.PodLookup{
    71  			Namespace: data.Target.Namespace,
    72  			Selector:  data.Target.Selector,
    73  			Container: data.Target.Container,
    74  		}
    75  
    76  		// Wait until the pod we are injecting data into becomes available
    77  		pods := c.WaitForPodsAndContainers(target, podFilterByInitContainer)
    78  		if len(pods) < 1 {
    79  			continue
    80  		}
    81  
    82  		// Inject into all the pods
    83  		for _, pod := range pods {
    84  			// Try to use the embedded kubectl if we can
    85  			jackalCommand, err := utils.GetFinalExecutableCommand()
    86  			kubectlBinPath := "kubectl"
    87  			if err != nil {
    88  				message.Warnf("Unable to get the jackal executable path, falling back to host kubectl: %s", err)
    89  			} else {
    90  				kubectlBinPath = fmt.Sprintf("%s tools kubectl", jackalCommand)
    91  			}
    92  			kubectlCmd := fmt.Sprintf("%s exec -i -n %s %s -c %s ", kubectlBinPath, data.Target.Namespace, pod.Name, data.Target.Container)
    93  
    94  			// Note that each command flag is separated to provide the widest cross-platform tar support
    95  			tarCmd := fmt.Sprintf("tar -c %s -f -", tarCompressFlag)
    96  			untarCmd := fmt.Sprintf("tar -x %s -v -f - -C %s", tarCompressFlag, data.Target.Path)
    97  
    98  			// Must create the target directory before trying to change to it for untar
    99  			mkdirCmd := fmt.Sprintf("%s -- mkdir -p %s", kubectlCmd, data.Target.Path)
   100  			if err := exec.CmdWithPrint(shell, append(shellArgs, mkdirCmd)...); err != nil {
   101  				message.Warnf("Unable to create the data injection target directory %s in pod %s", data.Target.Path, pod.Name)
   102  				continue iterator
   103  			}
   104  
   105  			cpPodCmd := fmt.Sprintf("%s -C %s . | %s -- %s",
   106  				tarCmd,
   107  				source,
   108  				kubectlCmd,
   109  				untarCmd,
   110  			)
   111  
   112  			// Do the actual data injection
   113  			if err := exec.CmdWithPrint(shell, append(shellArgs, cpPodCmd)...); err != nil {
   114  				message.Warnf("Error copying data into the pod %#v: %#v\n", pod.Name, err)
   115  				continue iterator
   116  			}
   117  
   118  			// Leave a marker in the target container for pods to track the sync action
   119  			cpPodCmd = fmt.Sprintf("%s -C %s %s | %s -- %s",
   120  				tarCmd,
   121  				componentPath.DataInjections,
   122  				config.GetDataInjectionMarker(),
   123  				kubectlCmd,
   124  				untarCmd,
   125  			)
   126  
   127  			if err := exec.CmdWithPrint(shell, append(shellArgs, cpPodCmd)...); err != nil {
   128  				message.Warnf("Error saving the jackal sync completion file after injection into pod %#v\n", pod.Name)
   129  				continue iterator
   130  			}
   131  		}
   132  
   133  		// Do not look for a specific container after injection in case they are running an init container
   134  		podOnlyTarget := k8s.PodLookup{
   135  			Namespace: data.Target.Namespace,
   136  			Selector:  data.Target.Selector,
   137  		}
   138  
   139  		// Block one final time to make sure at least one pod has come up and injected the data
   140  		// Using only the pod as the final selector because we don't know what the container name will be
   141  		// Still using the init container filter to make sure we have the right running pod
   142  		_ = c.WaitForPodsAndContainers(podOnlyTarget, podFilterByInitContainer)
   143  
   144  		// Cleanup now to reduce disk pressure
   145  		_ = os.RemoveAll(source)
   146  
   147  		// Return to stop the loop
   148  		return
   149  	}
   150  }