...

Source file src/edge-infra.dev/pkg/sds/lib/k8s/manifest/manifest.go

Documentation: edge-infra.dev/pkg/sds/lib/k8s/manifest

     1  package manifest
     2  
     3  import (
     4  	"fmt"
     5  	"io/fs"
     6  	"os"
     7  	"path/filepath"
     8  
     9  	"github.com/spf13/afero"
    10  	"k8s.io/apimachinery/pkg/runtime"
    11  	"sigs.k8s.io/yaml"
    12  
    13  	"edge-infra.dev/pkg/k8s/decoder"
    14  	"edge-infra.dev/pkg/k8s/unstructured"
    15  )
    16  
    17  type Manifest struct {
    18  	fs      afero.Fs
    19  	path    string
    20  	content runtime.Object
    21  	mode    os.FileMode
    22  }
    23  
    24  // New returns a new Manifest object. The content argument should one of:
    25  //
    26  // 1. Empty runtime.Object (e.g. v1.Pod{}) that represents the type of data you expect the Manifest
    27  // to contain. This is useful if you intend on loading or reading content at a later point.
    28  //
    29  // 2. Complete runtime.Object (e.g. v1.Pod{Name:xxx,...}) that represents the object that you would
    30  // like to start with. This is useful if you already know the content of a manifest that you would
    31  // like to write.
    32  //
    33  // The mode provided can be 0 only if the intention is to respect the original mode of the current
    34  // manifest (if one exists), or accept the default of 0600.
    35  func New(fs afero.Fs, path string, content runtime.Object, mode os.FileMode) Manifest {
    36  	return Manifest{
    37  		fs:      fs,
    38  		path:    path,
    39  		content: content,
    40  		mode:    mode,
    41  	}
    42  }
    43  
    44  // WithCreate wraps the given function with functionality to create a manifest. This
    45  // will overwrite the original if one was already in place. The provided function
    46  // should be passed the target object and can modify it as needed. If no mode is
    47  // set on the Manifest, 0600 will be used as default.
    48  func (m *Manifest) WithCreate(fn func(obj runtime.Object) error) error {
    49  	if err := fn(m.content); err != nil {
    50  		return err
    51  	}
    52  
    53  	if m.mode == 0 {
    54  		m.mode = 0600
    55  	}
    56  	return m.Write()
    57  }
    58  
    59  // WithUpdate wraps the given function with functionality to update a manifest. This
    60  // will read from the old manifest and the current structure will be passed in to the
    61  // provided function to modify as required. If a mode is not set on the Manifest, then
    62  // the mode of the original file will be respected.
    63  func (m *Manifest) WithUpdate(fn func(obj runtime.Object) error) error {
    64  	syncMode := (m.mode == 0)
    65  
    66  	if err := m.Read(syncMode); err != nil {
    67  		return fmt.Errorf("failed to read etcd manifest data: %w", err)
    68  	}
    69  
    70  	if err := fn(m.content); err != nil {
    71  		return err
    72  	}
    73  	return m.Write()
    74  }
    75  
    76  // Read reads a Kubernetes manifest file and decodes it into the type provided to
    77  // the Manifest. In the case of files that contain multiple manifests, Read will only
    78  // consider the first found manifest to decode into the object.
    79  //
    80  // The syncMode argument can be passed to tell the method whether you want to sync
    81  // the mode from the original file into the Manifest.
    82  func (m *Manifest) Read(syncMode bool) error {
    83  	manifestBytes, err := afero.ReadFile(m.fs, m.path)
    84  	if err != nil {
    85  		return err
    86  	}
    87  
    88  	if err := m.Load(manifestBytes); err != nil {
    89  		return err
    90  	}
    91  
    92  	if syncMode {
    93  		return m.syncFileMode()
    94  	}
    95  	return nil
    96  }
    97  
    98  // syncFileMode ensures that the file mode is correct for the file at the file
    99  // path by replacing the current mode with the actual value.
   100  func (m *Manifest) syncFileMode() error {
   101  	fileInfo, err := m.Stat()
   102  	if err != nil {
   103  		return fmt.Errorf("failed to stat file (%s): %w", m.path, err)
   104  	}
   105  
   106  	m.mode = fileInfo.Mode()
   107  	return nil
   108  }
   109  
   110  // Stat will return file information about the manifest at the provided
   111  // filepath.
   112  func (m *Manifest) Stat() (fs.FileInfo, error) {
   113  	file, err := m.fs.Open(m.path)
   114  	if err != nil {
   115  		return nil, fmt.Errorf("failed to open file: %w", err)
   116  	}
   117  
   118  	return file.Stat()
   119  }
   120  
   121  // Load will take the provided bytes (which should represent a manifest file)
   122  // and decode them into the Manifest object. In the case of files that contain
   123  // multiple manifests, Read will only consider the first found manifest to decode
   124  // into the object.
   125  func (m *Manifest) Load(manifestBytes []byte) error {
   126  	manifests, err := decoder.DecodeYAML(manifestBytes)
   127  	if err != nil {
   128  		return fmt.Errorf("failed to decode YAML: %w", err)
   129  	}
   130  	if len(manifests) == 0 {
   131  		return fmt.Errorf("no manifests found in file")
   132  	}
   133  
   134  	if err := runtime.DefaultUnstructuredConverter.FromUnstructured(manifests[0].UnstructuredContent(), m.content); err != nil {
   135  		return fmt.Errorf("failed to convert: %w", err)
   136  	}
   137  	return nil
   138  }
   139  
   140  // Write encodes the given pod object into the Kubernetes manifest file
   141  // at the defined path. This process is atomic and will write a temporary
   142  // file for the object before moving it over the original.
   143  func (m *Manifest) Write() error {
   144  	if m.content == nil {
   145  		return fmt.Errorf("unable to write nil content")
   146  	}
   147  	bytes, err := m.Bytes()
   148  	if err != nil {
   149  		return err
   150  	}
   151  
   152  	tmpFilename := "." + filepath.Base(m.path) + ".tmp"
   153  	tmpPath := filepath.Join(filepath.Dir(m.path), tmpFilename)
   154  	if err := afero.WriteFile(m.fs, tmpPath, bytes, m.mode); err != nil {
   155  		return err
   156  	}
   157  	return m.fs.Rename(tmpPath, m.path)
   158  }
   159  
   160  // Content will return the current object loaded into the Manifest.
   161  func (m *Manifest) Content() runtime.Object {
   162  	return m.content
   163  }
   164  
   165  // Bytes will return the current object loaded into the Manifest in []byte form.
   166  func (m *Manifest) Bytes() ([]byte, error) {
   167  	uobj := unstructured.Unstructured{}
   168  	obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(m.content)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	uobj.Object = obj
   173  
   174  	json, err := uobj.MarshalJSON()
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	return yaml.JSONToYAML(json)
   180  }
   181  

View as plain text