support kubeadm join dry-run

This commit is contained in:
Haleygo 2021-06-20 15:42:41 +08:00
parent 6a043332be
commit 95e000fd65
12 changed files with 198 additions and 69 deletions

View File

@ -20,6 +20,7 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"text/template"
@ -132,6 +133,7 @@ type joinOptions struct {
externalcfg *kubeadmapiv1.JoinConfiguration
joinControlPlane *kubeadmapiv1.JoinControlPlane
patchesDir string
dryRun bool
}
// compile-time assert that the local data object satisfies the phases data interface.
@ -147,6 +149,8 @@ type joinData struct {
ignorePreflightErrors sets.String
outputWriter io.Writer
patchesDir string
dryRun bool
dryRunDir string
}
// newCmdJoin returns "kubeadm join" command.
@ -295,6 +299,10 @@ func addJoinOtherFlags(flagSet *flag.FlagSet, joinOptions *joinOptions) {
&joinOptions.controlPlane, options.ControlPlane, joinOptions.controlPlane,
"Create a new control plane instance on this node",
)
flagSet.BoolVar(
&joinOptions.dryRun, options.DryRun, joinOptions.dryRun,
"Don't apply any changes; just output what would be done.",
)
options.AddPatchesFlag(flagSet, &joinOptions.patchesDir)
}
@ -445,12 +453,22 @@ func newJoinData(cmd *cobra.Command, args []string, opt *joinOptions, out io.Wri
}
}
// if dry running, creates a temporary folder to save kubeadm generated files
dryRunDir := ""
if opt.dryRun {
if dryRunDir, err = kubeadmconstants.CreateTempDirForKubeadm("", "kubeadm-join-dryrun"); err != nil {
return nil, errors.Wrap(err, "couldn't create a temporary directory on dryrun")
}
}
return &joinData{
cfg: cfg,
tlsBootstrapCfg: tlsBootstrapCfg,
ignorePreflightErrors: ignorePreflightErrorsSet,
outputWriter: out,
patchesDir: opt.patchesDir,
dryRun: opt.dryRun,
dryRunDir: dryRunDir,
}, nil
}
@ -467,6 +485,43 @@ func (j *joinData) Cfg() *kubeadmapi.JoinConfiguration {
return j.cfg
}
// DryRun returns the DryRun flag.
func (j *joinData) DryRun() bool {
return j.dryRun
}
// KubeConfigDir returns the path of the Kubernetes configuration folder or the temporary folder path in case of DryRun.
func (j *joinData) KubeConfigDir() string {
if j.dryRun {
return j.dryRunDir
}
return kubeadmconstants.KubernetesDir
}
// KubeletDir returns the path of the kubelet configuration folder or the temporary folder in case of DryRun.
func (j *joinData) KubeletDir() string {
if j.dryRun {
return j.dryRunDir
}
return kubeadmconstants.KubeletRunDirectory
}
// ManifestDir returns the path where manifest should be stored or the temporary folder path in case of DryRun.
func (j *joinData) ManifestDir() string {
if j.dryRun {
return j.dryRunDir
}
return kubeadmconstants.GetStaticPodDirectory()
}
// CertificateWriteDir returns the path where certs should be stored or the temporary folder path in case of DryRun.
func (j *joinData) CertificateWriteDir() string {
if j.dryRun {
return j.dryRunDir
}
return j.initCfg.CertificatesDir
}
// TLSBootstrapCfg returns the cluster-info (kubeconfig).
func (j *joinData) TLSBootstrapCfg() (*clientcmdapi.Config, error) {
if j.tlsBootstrapCfg != nil {
@ -497,7 +552,8 @@ func (j *joinData) ClientSet() (*clientset.Clientset, error) {
if j.clientSet != nil {
return j.clientSet, nil
}
path := kubeadmconstants.GetAdminKubeConfigPath()
path := filepath.Join(j.KubeConfigDir(), kubeadmconstants.AdminKubeConfigFileName)
client, err := kubeconfigutil.ClientSetFromFile(path)
if err != nil {
return nil, errors.Wrap(err, "[preflight] couldn't create Kubernetes client")

View File

@ -19,7 +19,6 @@ package phases
import (
"fmt"
"io"
"path/filepath"
"text/template"
"time"
@ -80,9 +79,12 @@ func runWaitControlPlanePhase(c workflow.RunData) error {
return errors.New("wait-control-plane phase invoked with an invalid data struct")
}
// If we're dry-running, print the generated manifests
if err := printFilesIfDryRunning(data); err != nil {
return errors.Wrap(err, "error printing files on dryrun")
// If we're dry-running, print the generated manifests.
// TODO: think of a better place to move this call - e.g. a hidden phase.
if data.DryRun() {
if err := dryrunutil.PrintFilesIfDryRunning(true /* needPrintManifest */, data.ManifestDir(), data.OutputWriter()); err != nil {
return errors.Wrap(err, "error printing files on dryrun")
}
}
// waiter holds the apiclient.Waiter implementation of choice, responsible for querying the API server in various ways and waiting for conditions to be fulfilled
@ -119,36 +121,6 @@ func runWaitControlPlanePhase(c workflow.RunData) error {
return nil
}
// printFilesIfDryRunning prints the Static Pod manifests to stdout and informs about the temporary directory to go and lookup
func printFilesIfDryRunning(data InitData) error {
if !data.DryRun() {
return nil
}
manifestDir := data.ManifestDir()
fmt.Printf("[dryrun] Wrote certificates, kubeconfig files and control plane manifests to the %q directory\n", manifestDir)
fmt.Println("[dryrun] The certificates or kubeconfig files would not be printed due to their sensitive nature")
fmt.Printf("[dryrun] Please examine the %q directory for details about what would be written\n", manifestDir)
// Print the contents of the upgraded manifests and pretend like they were in /etc/kubernetes/manifests
files := []dryrunutil.FileToPrint{}
// Print static pod manifests
for _, component := range kubeadmconstants.ControlPlaneComponents {
realPath := kubeadmconstants.GetStaticPodFilepath(component, manifestDir)
outputPath := kubeadmconstants.GetStaticPodFilepath(component, kubeadmconstants.GetStaticPodDirectory())
files = append(files, dryrunutil.NewFileToPrint(realPath, outputPath))
}
// Print kubelet config manifests
kubeletConfigFiles := []string{kubeadmconstants.KubeletConfigurationFileName, kubeadmconstants.KubeletEnvFileName}
for _, filename := range kubeletConfigFiles {
realPath := filepath.Join(manifestDir, filename)
outputPath := filepath.Join(kubeadmconstants.KubeletRunDirectory, filename)
files = append(files, dryrunutil.NewFileToPrint(realPath, outputPath))
}
return dryrunutil.PrintDryRunFiles(files, data.OutputWriter())
}
// newControlPlaneWaiter returns a new waiter that is used to wait on the control plane to boot up.
func newControlPlaneWaiter(dryRun bool, timeout time.Duration, client clientset.Interface, out io.Writer) (apiclient.Waiter, error) {
if dryRun {

View File

@ -66,5 +66,5 @@ func runCheckEtcdPhase(c workflow.RunData) error {
return err
}
return etcdphase.CheckLocalEtcdClusterStatus(client, &cfg.ClusterConfiguration)
return etcdphase.CheckLocalEtcdClusterStatus(client, data.CertificateWriteDir())
}

View File

@ -132,9 +132,13 @@ func runEtcdPhase(c workflow.RunData) error {
return nil
}
// Create the etcd data directory
if err := etcdutil.CreateDataDirectory(cfg.Etcd.Local.DataDir); err != nil {
return err
if !data.DryRun() {
// Create the etcd data directory
if err := etcdutil.CreateDataDirectory(cfg.Etcd.Local.DataDir); err != nil {
return err
}
} else {
fmt.Printf("[dryrun] Would ensure that %q directory is present\n", cfg.Etcd.Local.DataDir)
}
// Adds a new etcd instance; in order to do this the new etcd instance should be "announced" to
@ -147,8 +151,7 @@ func runEtcdPhase(c workflow.RunData) error {
// because it needs two members as majority to agree on the consensus. You will only see this behavior between the time
// etcdctl member add informs the cluster about the new member and the new member successfully establishing a connection to the
// existing one."
// TODO: add support for join dry-run: https://github.com/kubernetes/kubeadm/issues/2505
if err := etcdphase.CreateStackedEtcdStaticPodManifestFile(client, kubeadmconstants.GetStaticPodDirectory(), data.PatchesDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, false /* isDryRun */); err != nil {
if err := etcdphase.CreateStackedEtcdStaticPodManifestFile(client, data.ManifestDir(), data.PatchesDir(), cfg.NodeRegistration.Name, &cfg.ClusterConfiguration, &cfg.LocalAPIEndpoint, data.DryRun(), data.CertificateWriteDir()); err != nil {
return errors.Wrap(err, "error creating local etcd static pod manifest file")
}
@ -188,8 +191,12 @@ func runMarkControlPlanePhase(c workflow.RunData) error {
return err
}
if err := markcontrolplanephase.MarkControlPlane(client, cfg.NodeRegistration.Name, cfg.NodeRegistration.Taints); err != nil {
return errors.Wrap(err, "error applying control-plane label and taints")
if !data.DryRun() {
if err := markcontrolplanephase.MarkControlPlane(client, cfg.NodeRegistration.Name, cfg.NodeRegistration.Taints); err != nil {
return errors.Wrap(err, "error applying control-plane label and taints")
}
} else {
fmt.Printf("[dryrun] Would mark node %s as a control-plane\n", cfg.NodeRegistration.Name)
}
return nil

View File

@ -18,6 +18,7 @@ package phases
import (
"fmt"
"path/filepath"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow"
@ -189,17 +190,21 @@ func runControlPlanePrepareControlPlaneSubphase(c workflow.RunData) error {
return err
}
fmt.Printf("[control-plane] Using manifest folder %q\n", kubeadmconstants.GetStaticPodDirectory())
fmt.Printf("[control-plane] Using manifest folder %q\n", data.ManifestDir())
// If we're dry-running, set CertificatesDir to default value to get the right cert path in static pod yaml
if data.DryRun() {
cfg.CertificatesDir = filepath.Join(kubeadmconstants.KubernetesDir, kubeadmconstants.DefaultCertificateDir)
}
for _, component := range kubeadmconstants.ControlPlaneComponents {
fmt.Printf("[control-plane] Creating static Pod manifest for %q\n", component)
err := controlplane.CreateStaticPodFiles(
kubeadmconstants.GetStaticPodDirectory(),
data.ManifestDir(),
data.PatchesDir(),
&cfg.ClusterConfiguration,
&cfg.LocalAPIEndpoint,
// TODO: add support for join dry-run:
// https://github.com/kubernetes/kubeadm/issues/2505
false,
data.DryRun(),
component,
)
if err != nil {
@ -225,6 +230,11 @@ func runControlPlanePrepareDownloadCertsPhaseLocal(c workflow.RunData) error {
return err
}
// If we're dry-running, download certs to tmp dir
if data.DryRun() {
cfg.CertificatesDir = data.CertificateWriteDir()
}
client, err := bootstrapClient(data)
if err != nil {
return err
@ -275,12 +285,12 @@ func runControlPlanePrepareKubeconfigPhaseLocal(c workflow.RunData) error {
}
fmt.Println("[kubeconfig] Generating kubeconfig files")
fmt.Printf("[kubeconfig] Using kubeconfig folder %q\n", kubeadmconstants.KubernetesDir)
fmt.Printf("[kubeconfig] Using kubeconfig folder %q\n", data.KubeConfigDir())
// Generate kubeconfig files for controller manager, scheduler and for the admin/kubeadm itself
// NB. The kubeconfig file for kubelet will be generated by the TLS bootstrap process in
// following steps of the join --control-plane workflow
if err := kubeconfigphase.CreateJoinControlPlaneKubeConfigFiles(kubeadmconstants.KubernetesDir, cfg); err != nil {
if err := kubeconfigphase.CreateJoinControlPlaneKubeConfigFiles(data.KubeConfigDir(), cfg); err != nil {
return errors.Wrap(err, "error generating kubeconfig files")
}

View File

@ -37,4 +37,9 @@ type JoinData interface {
IgnorePreflightErrors() sets.String
OutputWriter() io.Writer
PatchesDir() string
DryRun() bool
KubeConfigDir() string
KubeletDir() string
ManifestDir() string
CertificateWriteDir() string
}

View File

@ -40,3 +40,8 @@ func (j *testJoinData) ClientSet() (*clientset.Clientset, error) { return
func (j *testJoinData) IgnorePreflightErrors() sets.String { return nil }
func (j *testJoinData) OutputWriter() io.Writer { return nil }
func (j *testJoinData) PatchesDir() string { return "" }
func (t *testJoinData) DryRun() bool { return false }
func (t *testJoinData) KubeConfigDir() string { return "" }
func (t *testJoinData) KubeletDir() string { return "" }
func (t *testJoinData) ManifestDir() string { return "" }
func (t *testJoinData) CertificateWriteDir() string { return "" }

View File

@ -20,6 +20,7 @@ import (
"context"
"fmt"
"os"
"path/filepath"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
@ -28,6 +29,7 @@ import (
kubeletphase "k8s.io/kubernetes/cmd/kubeadm/app/phases/kubelet"
patchnodephase "k8s.io/kubernetes/cmd/kubeadm/app/phases/patchnode"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
dryrunutil "k8s.io/kubernetes/cmd/kubeadm/app/util/dryrun"
kubeconfigutil "k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig"
v1 "k8s.io/api/core/v1"
@ -103,7 +105,12 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) {
if err != nil {
return err
}
bootstrapKubeConfigFile := kubeadmconstants.GetBootstrapKubeletKubeConfigPath()
data, ok := c.(JoinData)
if !ok {
return errors.New("kubelet-start phase invoked with an invalid data struct")
}
bootstrapKubeConfigFile := filepath.Join(data.KubeConfigDir(), kubeadmconstants.KubeletBootstrapKubeConfigFileName)
// Deletes the bootstrapKubeConfigFile, so the credential used for TLS bootstrap is removed from disk
defer os.Remove(bootstrapKubeConfigFile)
@ -116,9 +123,16 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) {
// Write the ca certificate to disk so kubelet can use it for authentication
cluster := tlsBootstrapCfg.Contexts[tlsBootstrapCfg.CurrentContext].Cluster
if _, err := os.Stat(cfg.CACertPath); os.IsNotExist(err) {
klog.V(1).Infof("[kubelet-start] writing CA certificate at %s", cfg.CACertPath)
if err := certutil.WriteCert(cfg.CACertPath, tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil {
// If we're dry-running, write ca cert in tmp
caPath := cfg.CACertPath
if data.DryRun() {
caPath = filepath.Join(data.CertificateWriteDir(), kubeadmconstants.CACertName)
}
if _, err := os.Stat(caPath); os.IsNotExist(err) {
klog.V(1).Infof("[kubelet-start] writing CA certificate at %s", caPath)
if err := certutil.WriteCert(caPath, tlsBootstrapCfg.Clusters[cluster].CertificateAuthorityData); err != nil {
return errors.Wrap(err, "couldn't save the CA certificate to disk")
}
}
@ -152,11 +166,15 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) {
// Configure the kubelet. In this short timeframe, kubeadm is trying to stop/restart the kubelet
// Try to stop the kubelet service so no race conditions occur when configuring it
klog.V(1).Infoln("[kubelet-start] Stopping the kubelet")
kubeletphase.TryStopKubelet()
if !data.DryRun() {
klog.V(1).Infoln("[kubelet-start] Stopping the kubelet")
kubeletphase.TryStopKubelet()
} else {
fmt.Println("[dryrun] Would stop the kubelet")
}
// Write the configuration for the kubelet (using the bootstrap token credentials) to disk so the kubelet can start
if err := kubeletphase.WriteConfigToDisk(&initCfg.ClusterConfiguration, kubeadmconstants.KubeletRunDirectory); err != nil {
if err := kubeletphase.WriteConfigToDisk(&initCfg.ClusterConfiguration, data.KubeletDir()); err != nil {
return err
}
@ -164,10 +182,20 @@ func runKubeletStartJoinPhase(c workflow.RunData) (returnErr error) {
// register the joining node with the specified taints if the node
// is not a control-plane. The mark-control-plane phase will register the taints otherwise.
registerTaintsUsingFlags := cfg.ControlPlane == nil
if err := kubeletphase.WriteKubeletDynamicEnvFile(&initCfg.ClusterConfiguration, &initCfg.NodeRegistration, registerTaintsUsingFlags, kubeadmconstants.KubeletRunDirectory); err != nil {
if err := kubeletphase.WriteKubeletDynamicEnvFile(&initCfg.ClusterConfiguration, &initCfg.NodeRegistration, registerTaintsUsingFlags, data.KubeletDir()); err != nil {
return err
}
if data.DryRun() {
fmt.Println("[dryrun] Would start the kubelet")
// If we're dry-running, print the kubelet config manifests and print static pod manifests if joining a control plane.
// TODO: think of a better place to move this call - e.g. a hidden phase.
if err := dryrunutil.PrintFilesIfDryRunning(cfg.ControlPlane != nil, data.ManifestDir(), data.OutputWriter()); err != nil {
return errors.Wrap(err, "error printing files on dryrun")
}
return nil
}
// Try to start the kubelet service in case it's inactive
fmt.Println("[kubelet-start] Starting the kubelet")
kubeletphase.TryStartKubelet()

View File

@ -123,6 +123,11 @@ func runPreflight(c workflow.RunData) error {
return err
}
if j.DryRun() {
fmt.Println("[preflight] Would pull the required images (like 'kubeadm config images pull')")
return nil
}
fmt.Println("[preflight] Pulling images required for setting up a Kubernetes cluster")
fmt.Println("[preflight] This might take a minute or two, depending on the speed of your internet connection")
fmt.Println("[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'")

View File

@ -47,6 +47,9 @@ const (
// CertificateValidity defines the validity for all the signed certificates generated by kubeadm
CertificateValidity = time.Hour * 24 * 365
// DefaultCertificateDir defines default certificate directory
DefaultCertificateDir = "pki"
// CACertAndKeyBaseName defines certificate authority base name
CACertAndKeyBaseName = "ca"
// CACertName defines certificate name

View File

@ -67,12 +67,12 @@ func CreateLocalEtcdStaticPodManifestFile(manifestDir, patchesDir string, nodeNa
}
// CheckLocalEtcdClusterStatus verifies health state of local/stacked etcd cluster before installing a new etcd member
func CheckLocalEtcdClusterStatus(client clientset.Interface, cfg *kubeadmapi.ClusterConfiguration) error {
func CheckLocalEtcdClusterStatus(client clientset.Interface, certificatesDir string) error {
klog.V(1).Info("[etcd] Checking etcd cluster health")
// creates an etcd client that connects to all the local/stacked etcd members
klog.V(1).Info("creating etcd client that connects to etcd pods")
etcdClient, err := etcdutil.NewFromCluster(client, cfg.CertificatesDir)
etcdClient, err := etcdutil.NewFromCluster(client, certificatesDir)
if err != nil {
return err
}
@ -134,32 +134,40 @@ func RemoveStackedEtcdMemberFromCluster(client clientset.Interface, cfg *kubeadm
// CreateStackedEtcdStaticPodManifestFile will write local etcd static pod manifest file
// for an additional etcd member that is joining an existing local/stacked etcd cluster.
// Other members of the etcd cluster will be notified of the joining node in beforehand as well.
func CreateStackedEtcdStaticPodManifestFile(client clientset.Interface, manifestDir, patchesDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint, isDryRun bool) error {
func CreateStackedEtcdStaticPodManifestFile(client clientset.Interface, manifestDir, patchesDir string, nodeName string, cfg *kubeadmapi.ClusterConfiguration, endpoint *kubeadmapi.APIEndpoint, isDryRun bool, certificatesDir string) error {
// creates an etcd client that connects to all the local/stacked etcd members
klog.V(1).Info("creating etcd client that connects to etcd pods")
etcdClient, err := etcdutil.NewFromCluster(client, cfg.CertificatesDir)
etcdClient, err := etcdutil.NewFromCluster(client, certificatesDir)
if err != nil {
return err
}
etcdPeerAddress := etcdutil.GetPeerURL(endpoint)
klog.V(1).Infof("[etcd] Adding etcd member: %s", etcdPeerAddress)
var cluster []etcdutil.Member
cluster, err = etcdClient.AddMember(nodeName, etcdPeerAddress)
if err != nil {
return err
if isDryRun {
fmt.Printf("[dryrun] Would add etcd member: %s\n", etcdPeerAddress)
} else {
klog.V(1).Infof("[etcd] Adding etcd member: %s", etcdPeerAddress)
cluster, err = etcdClient.AddMember(nodeName, etcdPeerAddress)
if err != nil {
return err
}
fmt.Println("[etcd] Announced new etcd member joining to the existing etcd cluster")
klog.V(1).Infof("Updated etcd member list: %v", cluster)
}
fmt.Println("[etcd] Announced new etcd member joining to the existing etcd cluster")
klog.V(1).Infof("Updated etcd member list: %v", cluster)
fmt.Printf("[etcd] Creating static Pod manifest for %q\n", kubeadmconstants.Etcd)
if err := prepareAndWriteEtcdStaticPod(manifestDir, patchesDir, cfg, endpoint, nodeName, cluster, isDryRun); err != nil {
return err
}
if isDryRun {
fmt.Println("[dryrun] Would wait for the new etcd member to join the cluster")
return nil
}
fmt.Printf("[etcd] Waiting for the new etcd member to join the cluster. This can take up to %v\n", etcdHealthyCheckInterval*etcdHealthyCheckRetries)
if _, err := etcdClient.WaitForClusterAvailable(etcdHealthyCheckRetries, etcdHealthyCheckInterval); err != nil {
return err

View File

@ -24,6 +24,7 @@ import (
"time"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/util/apiclient"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -139,3 +140,32 @@ func (w *Waiter) WaitForStaticPodSingleHash(_ string, _ string) (string, error)
func (w *Waiter) WaitForStaticPodHashChange(_, _, _ string) error {
return nil
}
// PrintFilesIfDryRunning prints the static pod manifests to stdout and informs about the temporary directory to go and lookup when dry running
func PrintFilesIfDryRunning(needPrintManifest bool, manifestDir string, outputWriter io.Writer) error {
var files []FileToPrint
// Print static pod manifests if it is a control plane
if needPrintManifest {
fmt.Printf("[dryrun] Wrote certificates, kubeconfig files and control plane manifests to the %q directory\n", manifestDir)
for _, component := range kubeadmconstants.ControlPlaneComponents {
realPath := kubeadmconstants.GetStaticPodFilepath(component, manifestDir)
outputPath := kubeadmconstants.GetStaticPodFilepath(component, kubeadmconstants.GetStaticPodDirectory())
files = append(files, NewFileToPrint(realPath, outputPath))
}
} else {
fmt.Printf("[dryrun] Wrote certificates and kubeconfig files to the %q directory\n", manifestDir)
}
fmt.Println("[dryrun] The certificates or kubeconfig files would not be printed due to their sensitive nature")
fmt.Printf("[dryrun] Please examine the %q directory for details about what would be written\n", manifestDir)
// Print kubelet config manifests
kubeletConfigFiles := []string{kubeadmconstants.KubeletConfigurationFileName, kubeadmconstants.KubeletEnvFileName}
for _, filename := range kubeletConfigFiles {
realPath := filepath.Join(manifestDir, filename)
outputPath := filepath.Join(kubeadmconstants.KubeletRunDirectory, filename)
files = append(files, NewFileToPrint(realPath, outputPath))
}
return PrintDryRunFiles(files, outputWriter)
}