github.com/crossplane/upjet@v1.3.0/pkg/migration/filesystem.go (about)

     1  // SPDX-FileCopyrightText: 2023 The Crossplane Authors <https://crossplane.io>
     2  //
     3  // SPDX-License-Identifier: Apache-2.0
     4  
     5  package migration
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"github.com/pkg/errors"
    14  	"github.com/spf13/afero"
    15  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    16  	"k8s.io/apimachinery/pkg/util/yaml"
    17  	sigsyaml "sigs.k8s.io/yaml"
    18  )
    19  
    20  var (
    21  	_ Source = &FileSystemSource{}
    22  	_ Target = &FileSystemTarget{}
    23  )
    24  
    25  // FileSystemSource is a source implementation to read resources from filesystem
    26  type FileSystemSource struct {
    27  	index int
    28  	items []UnstructuredWithMetadata
    29  	afero afero.Afero
    30  }
    31  
    32  // FileSystemSourceOption allows you to configure FileSystemSource
    33  type FileSystemSourceOption func(*FileSystemSource)
    34  
    35  // FsWithFileSystem configures the filesystem to use. Used mostly for testing.
    36  func FsWithFileSystem(f afero.Fs) FileSystemSourceOption {
    37  	return func(fs *FileSystemSource) {
    38  		fs.afero = afero.Afero{Fs: f}
    39  	}
    40  }
    41  
    42  // NewFileSystemSource returns a FileSystemSource
    43  func NewFileSystemSource(dir string, opts ...FileSystemSourceOption) (*FileSystemSource, error) {
    44  	fs := &FileSystemSource{
    45  		afero: afero.Afero{Fs: afero.NewOsFs()},
    46  	}
    47  	for _, f := range opts {
    48  		f(fs)
    49  	}
    50  
    51  	if err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
    52  		if err != nil {
    53  			return errors.Wrap(err, fmt.Sprintf("cannot read %s", path))
    54  		}
    55  
    56  		if info.IsDir() {
    57  			return nil
    58  		}
    59  
    60  		data, err := fs.afero.ReadFile(path)
    61  		if err != nil {
    62  			return errors.Wrap(err, "cannot read source file")
    63  		}
    64  
    65  		decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewBufferString(string(data)), 1024)
    66  		u := &unstructured.Unstructured{}
    67  		if err := decoder.Decode(&u); err != nil {
    68  			return errors.Wrap(err, "cannot decode read data")
    69  		}
    70  
    71  		fs.items = append(fs.items, UnstructuredWithMetadata{
    72  			Object: *u,
    73  			Metadata: Metadata{
    74  				Path:     path,
    75  				Category: getCategory(*u),
    76  			},
    77  		})
    78  
    79  		return nil
    80  	}); err != nil {
    81  		return nil, errors.Wrap(err, "cannot read source directory")
    82  	}
    83  
    84  	return fs, nil
    85  }
    86  
    87  // HasNext checks the next item
    88  func (fs *FileSystemSource) HasNext() (bool, error) {
    89  	return fs.index < len(fs.items), nil
    90  }
    91  
    92  // Next returns the next item of slice
    93  func (fs *FileSystemSource) Next() (UnstructuredWithMetadata, error) {
    94  	if hasNext, _ := fs.HasNext(); hasNext {
    95  		item := fs.items[fs.index]
    96  		fs.index++
    97  		return item, nil
    98  	}
    99  	return UnstructuredWithMetadata{}, errors.New("no more elements")
   100  }
   101  
   102  // Reset resets the source so that resources can be reread from the beginning.
   103  func (fs *FileSystemSource) Reset() error {
   104  	fs.index = 0
   105  	return nil
   106  }
   107  
   108  // FileSystemTarget is a target implementation to write/patch/delete resources to file system
   109  type FileSystemTarget struct {
   110  	afero  afero.Afero
   111  	parent string
   112  }
   113  
   114  // FileSystemTargetOption allows you to configure FileSystemTarget
   115  type FileSystemTargetOption func(*FileSystemTarget)
   116  
   117  // FtWithFileSystem configures the filesystem to use. Used mostly for testing.
   118  func FtWithFileSystem(f afero.Fs) FileSystemTargetOption {
   119  	return func(ft *FileSystemTarget) {
   120  		ft.afero = afero.Afero{Fs: f}
   121  	}
   122  }
   123  
   124  // WithParentDirectory configures the parent directory for the FileSystemTarget
   125  func WithParentDirectory(parent string) FileSystemTargetOption {
   126  	return func(ft *FileSystemTarget) {
   127  		ft.parent = parent
   128  	}
   129  }
   130  
   131  // NewFileSystemTarget returns a FileSystemTarget
   132  func NewFileSystemTarget(opts ...FileSystemTargetOption) *FileSystemTarget {
   133  	ft := &FileSystemTarget{
   134  		afero: afero.Afero{Fs: afero.NewOsFs()},
   135  	}
   136  	for _, f := range opts {
   137  		f(ft)
   138  	}
   139  	return ft
   140  }
   141  
   142  // Put writes input to filesystem
   143  func (ft *FileSystemTarget) Put(o UnstructuredWithMetadata) error {
   144  	b, err := sigsyaml.Marshal(o.Object.Object)
   145  	if err != nil {
   146  		return errors.Wrap(err, "cannot marshal object")
   147  	}
   148  	if err := os.MkdirAll(filepath.Join(ft.parent, filepath.Dir(o.Metadata.Path)), 0o750); err != nil {
   149  		return errors.Wrapf(err, "cannot mkdirall: %s", filepath.Dir(o.Metadata.Path))
   150  	}
   151  	if o.Metadata.Parents != "" {
   152  		f, err := ft.afero.OpenFile(filepath.Join(ft.parent, o.Metadata.Path), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
   153  		if err != nil {
   154  			return errors.Wrap(err, "cannot open file")
   155  		}
   156  
   157  		defer f.Close() //nolint:errcheck
   158  
   159  		if _, err = fmt.Fprintf(f, "\n---\n\n%s", string(b)); err != nil {
   160  			return errors.Wrap(err, "cannot write file")
   161  		}
   162  	} else {
   163  		f, err := ft.afero.Create(filepath.Join(ft.parent, o.Metadata.Path))
   164  		if err != nil {
   165  			return errors.Wrap(err, "cannot create file")
   166  		}
   167  		if _, err := f.Write(b); err != nil {
   168  			return errors.Wrap(err, "cannot write file")
   169  		}
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  // Delete deletes a file from filesystem
   176  func (ft *FileSystemTarget) Delete(o UnstructuredWithMetadata) error {
   177  	return ft.afero.Remove(o.Metadata.Path)
   178  }