package chariot import ( "crypto/md5" //nolint:gosec "fmt" "gopkg.in/yaml.v2" "edge-infra.dev/pkg/edge/constants" ) const ( OperationCreate = "CREATE" OperationDelete = "DELETE" ) // Request is a Chariot request that is encoded in JSON and pulled from PubSub. type Request struct { // Operation determines how Chariot handles the request. The following options are supported: // - CREATE // - DELETE // // For `CREATE` operations, Chariot stores each object idempotently in the desired location. // // For `DELETE` operations, Chariot determines the location of objects to delete by parsing // each of the provided Yaml object's Group/Version, Kind, Name, and Namespace values. Operation string `json:"operation"` // Objects is a slice of Base64 encoded K8S yaml files. Objects [][]byte `json:"objects"` // Banner represents the bucket that objects are located within. Banner string `json:"banner"` // Cluster is an optional field for cluster-specific objects. Cluster string `json:"cluster,omitempty"` // Owner is a string identifying the entity that created the object. // // example: `cluster-controller`, `banner-controller`, etc Owner string `json:"owner"` // Dir is a custom directory to place objects. For backwards compatibility, when Dir is empty, the "chariot" directory is applied. Dir string `json:"dir,omitempty"` // Notify is an optional field to notify the edge agent of changes to an object to be reconciled Notify bool `json:"notify,omitempty"` } const ( defaultChariotDir = "chariot" locationFormatBannerWideObjects = "gs://%s/%s/%s.yaml" locationFormatClusterWideObjects = "gs://%s/%s/%s/%s.yaml" ) // whitelistedDirs is a map of request Dir values that are accepted. var whitelistedDirs = map[string]bool{ defaultChariotDir: true, constants.KustomizationsDir: true, constants.ShipmentKustomizationDir: true, constants.ExternalSecretKustomizationDir: true, } // Validate the request, sanitize data, and set default values. func (r *Request) Validate() error { if "" == r.Operation { return fmt.Errorf("Operation must not be empty") } switch r.Operation { case OperationCreate: case OperationDelete: default: return fmt.Errorf("Unsupported Operation %q", r.Operation) } if 0 == len(r.Objects) { return fmt.Errorf("Objects must not be empty") } if "" == r.Banner { return fmt.Errorf("Banner must not be empty") } if "" == r.Owner { return fmt.Errorf("Owner must not be empty") } if "" == r.Dir { // The empty string is a valid Dir value. r.Dir = defaultChariotDir } if !whitelistedDirs[r.Dir] { return fmt.Errorf("The provided directory is not whitelisted: %q", r.Dir) } // Ensure the objects input is parsable yaml. for _, obj := range r.Objects { var gvknn, err = ParseYamlGVKNN(obj) if err != nil { return err } else if err = gvknn.Validate(); err != nil { return err } } return nil } // StorageObjects returns a slice of StorageObject, containing each of the Request.Objects members. // // Each StorageObject.Location field is calculated using the Request.Banner, Request.Cluster, and the object's YamlGVKNN hash. func (r *Request) StorageObjects() ([]StorageObject, error) { // Validate the request to ensure the Dir is set correctly. if err := r.Validate(); err != nil { return nil, err } var so []StorageObject for _, obj := range r.Objects { var gvknn, err = ParseYamlGVKNN(obj) if err != nil { return nil, err } else if err = gvknn.Validate(); err != nil { return nil, err } var location = FmtStorageLocation(r.Banner, r.Cluster, r.Dir, gvknn.Hash()) so = append(so, StorageObject{ Location: location, Content: string(obj), }) } return so, nil } // YamlGVKNN is a yaml object that is used to determine the Group/Version Kind Name Namespace information used when // naming storage objects based on a hash of this data. type YamlGVKNN struct { // APIVersion contains both the Group and the Version separated by a slash. i.e. "group/version" // // See: https://github.com/kubernetes/design-proposals-archive/blob/main/api-machinery/api-group.md APIVersion string `yaml:"apiVersion"` Kind string `yaml:"kind"` Metadata struct { Name string `yaml:"name"` Namespace string `yaml:"namespace"` } `yaml:"metadata"` } // ParseYamlGVKNN parses a yaml file into a simple object containing the Group/Version Kind Name Namespace information. func ParseYamlGVKNN(y []byte) (YamlGVKNN, error) { var parsed YamlGVKNN err := yaml.Unmarshal(y, &parsed) return parsed, err } func (y YamlGVKNN) Validate() error { if "" == y.APIVersion { return fmt.Errorf("apiVersion must not be empty") } if "" == y.Kind { return fmt.Errorf("kind must not be empty") } if "" == y.Metadata.Name { return fmt.Errorf("metadata.name must not be empty") } return nil } // HashFormatGVKNN is a formula to hash Group/Version Kind Name Namespace (GVKNN) information. // // The formula uses newline characters and escapes the GVKNN fields within quotes in order to produce a collision proof hash. const HashFormatGVKNN = "chariot\ngroup/version:%q\nkind:%q\nmetadata.name:%q\nmetadata.namespace:%q\nend" // Hash uses the HashFormatGVKNN format to hash GVKNN data in a reproducible manner. func (y YamlGVKNN) Hash() string { var s = fmt.Sprintf(HashFormatGVKNN, y.APIVersion, y.Kind, y.Metadata.Name, y.Metadata.Namespace) var m = md5.Sum([]byte(s)) //nolint:gosec // not subject to malicious input return fmt.Sprintf("%x", m) } // FmtStorageLocation returns the path of an object in storage using the format, gs://///.yaml // if 'cluster' is not empty. Otherwise the format, gs:////.yaml, is used. func FmtStorageLocation(banner, cluster, dir, chariotID string) string { if dir == "" { dir = defaultChariotDir } var location string if cluster == "" { location = fmt.Sprintf(locationFormatBannerWideObjects, banner, dir, chariotID) } else { location = fmt.Sprintf(locationFormatClusterWideObjects, banner, cluster, dir, chariotID) } return location }