...

Source file src/edge-infra.dev/pkg/edge/chariot/request.go

Documentation: edge-infra.dev/pkg/edge/chariot

     1  package chariot
     2  
     3  import (
     4  	"crypto/md5" //nolint:gosec
     5  	"fmt"
     6  
     7  	"gopkg.in/yaml.v2"
     8  
     9  	"edge-infra.dev/pkg/edge/constants"
    10  )
    11  
    12  const (
    13  	OperationCreate = "CREATE"
    14  	OperationDelete = "DELETE"
    15  )
    16  
    17  // Request is a Chariot request that is encoded in JSON and pulled from PubSub.
    18  type Request struct {
    19  	// Operation determines how Chariot handles the request. The following options are supported:
    20  	//  - CREATE
    21  	//  - DELETE
    22  	//
    23  	// For `CREATE` operations, Chariot stores each object idempotently in the desired location.
    24  	//
    25  	// For `DELETE` operations, Chariot determines the location of objects to delete by parsing
    26  	// each of the provided Yaml object's Group/Version, Kind, Name, and Namespace values.
    27  	Operation string `json:"operation"`
    28  
    29  	// Objects is a slice of Base64 encoded K8S yaml files.
    30  	Objects [][]byte `json:"objects"`
    31  
    32  	// Banner represents the bucket that objects are located within.
    33  	Banner string `json:"banner"`
    34  
    35  	// Cluster is an optional field for cluster-specific objects.
    36  	Cluster string `json:"cluster,omitempty"`
    37  
    38  	// Owner is a string identifying the entity that created the object.
    39  	//
    40  	// example: `cluster-controller`, `banner-controller`, etc
    41  	Owner string `json:"owner"`
    42  
    43  	// Dir is a custom directory to place objects. For backwards compatibility, when Dir is empty, the "chariot" directory is applied.
    44  	Dir string `json:"dir,omitempty"`
    45  
    46  	// Notify is an optional field to notify the edge agent of changes to an object to be reconciled
    47  	Notify bool `json:"notify,omitempty"`
    48  }
    49  
    50  const (
    51  	defaultChariotDir                = "chariot"
    52  	locationFormatBannerWideObjects  = "gs://%s/%s/%s.yaml"
    53  	locationFormatClusterWideObjects = "gs://%s/%s/%s/%s.yaml"
    54  )
    55  
    56  // whitelistedDirs is a map of request Dir values that are accepted.
    57  var whitelistedDirs = map[string]bool{
    58  	defaultChariotDir:                        true,
    59  	constants.KustomizationsDir:              true,
    60  	constants.ShipmentKustomizationDir:       true,
    61  	constants.ExternalSecretKustomizationDir: true,
    62  }
    63  
    64  // Validate the request, sanitize data, and set default values.
    65  func (r *Request) Validate() error {
    66  	if "" == r.Operation {
    67  		return fmt.Errorf("Operation must not be empty")
    68  	}
    69  
    70  	switch r.Operation {
    71  	case OperationCreate:
    72  	case OperationDelete:
    73  	default:
    74  		return fmt.Errorf("Unsupported Operation %q", r.Operation)
    75  	}
    76  
    77  	if 0 == len(r.Objects) {
    78  		return fmt.Errorf("Objects must not be empty")
    79  	}
    80  
    81  	if "" == r.Banner {
    82  		return fmt.Errorf("Banner must not be empty")
    83  	}
    84  
    85  	if "" == r.Owner {
    86  		return fmt.Errorf("Owner must not be empty")
    87  	}
    88  
    89  	if "" == r.Dir {
    90  		// The empty string is a valid Dir value.
    91  		r.Dir = defaultChariotDir
    92  	}
    93  	if !whitelistedDirs[r.Dir] {
    94  		return fmt.Errorf("The provided directory is not whitelisted: %q", r.Dir)
    95  	}
    96  
    97  	// Ensure the objects input is parsable yaml.
    98  	for _, obj := range r.Objects {
    99  		var gvknn, err = ParseYamlGVKNN(obj)
   100  		if err != nil {
   101  			return err
   102  		} else if err = gvknn.Validate(); err != nil {
   103  			return err
   104  		}
   105  	}
   106  	return nil
   107  }
   108  
   109  // StorageObjects returns a slice of StorageObject, containing each of the Request.Objects members.
   110  //
   111  // Each StorageObject.Location field is calculated using the Request.Banner, Request.Cluster, and the object's YamlGVKNN hash.
   112  func (r *Request) StorageObjects() ([]StorageObject, error) {
   113  	// Validate the request to ensure the Dir is set correctly.
   114  	if err := r.Validate(); err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	var so []StorageObject
   119  	for _, obj := range r.Objects {
   120  		var gvknn, err = ParseYamlGVKNN(obj)
   121  		if err != nil {
   122  			return nil, err
   123  		} else if err = gvknn.Validate(); err != nil {
   124  			return nil, err
   125  		}
   126  		var location = FmtStorageLocation(r.Banner, r.Cluster, r.Dir, gvknn.Hash())
   127  		so = append(so, StorageObject{
   128  			Location: location,
   129  			Content:  string(obj),
   130  		})
   131  	}
   132  	return so, nil
   133  }
   134  
   135  // YamlGVKNN is a yaml object that is used to determine the Group/Version Kind Name Namespace information used when
   136  // naming storage objects based on a hash of this data.
   137  type YamlGVKNN struct {
   138  	// APIVersion contains both the Group and the Version separated by a slash. i.e. "group/version"
   139  	//
   140  	// See: https://github.com/kubernetes/design-proposals-archive/blob/main/api-machinery/api-group.md
   141  	APIVersion string `yaml:"apiVersion"`
   142  	Kind       string `yaml:"kind"`
   143  	Metadata   struct {
   144  		Name      string `yaml:"name"`
   145  		Namespace string `yaml:"namespace"`
   146  	} `yaml:"metadata"`
   147  }
   148  
   149  // ParseYamlGVKNN parses a yaml file into a simple object containing the Group/Version Kind Name Namespace information.
   150  func ParseYamlGVKNN(y []byte) (YamlGVKNN, error) {
   151  	var parsed YamlGVKNN
   152  	err := yaml.Unmarshal(y, &parsed)
   153  	return parsed, err
   154  }
   155  
   156  func (y YamlGVKNN) Validate() error {
   157  	if "" == y.APIVersion {
   158  		return fmt.Errorf("apiVersion must not be empty")
   159  	}
   160  
   161  	if "" == y.Kind {
   162  		return fmt.Errorf("kind must not be empty")
   163  	}
   164  
   165  	if "" == y.Metadata.Name {
   166  		return fmt.Errorf("metadata.name must not be empty")
   167  	}
   168  
   169  	return nil
   170  }
   171  
   172  // HashFormatGVKNN is a formula to hash Group/Version Kind Name Namespace (GVKNN) information.
   173  //
   174  // The formula uses newline characters and escapes the GVKNN fields within quotes in order to produce a collision proof hash.
   175  const HashFormatGVKNN = "chariot\ngroup/version:%q\nkind:%q\nmetadata.name:%q\nmetadata.namespace:%q\nend"
   176  
   177  // Hash uses the HashFormatGVKNN format to hash GVKNN data in a reproducible manner.
   178  func (y YamlGVKNN) Hash() string {
   179  	var s = fmt.Sprintf(HashFormatGVKNN, y.APIVersion, y.Kind, y.Metadata.Name, y.Metadata.Namespace)
   180  	var m = md5.Sum([]byte(s)) //nolint:gosec // not subject to malicious input
   181  	return fmt.Sprintf("%x", m)
   182  }
   183  
   184  // FmtStorageLocation returns the path of an object in storage using the format, gs://<banner>/<cluster>/<dir>/<chariotID>.yaml
   185  // if 'cluster' is not empty. Otherwise the format, gs://<banner>/<dir>/<chariotID>.yaml, is used.
   186  func FmtStorageLocation(banner, cluster, dir, chariotID string) string {
   187  	if dir == "" {
   188  		dir = defaultChariotDir
   189  	}
   190  	var location string
   191  	if cluster == "" {
   192  		location = fmt.Sprintf(locationFormatBannerWideObjects, banner, dir, chariotID)
   193  	} else {
   194  		location = fmt.Sprintf(locationFormatClusterWideObjects, banner, cluster, dir, chariotID)
   195  	}
   196  	return location
   197  }
   198  

View as plain text