...

Source file src/edge-infra.dev/pkg/edge/api/services/channels/types.go

Documentation: edge-infra.dev/pkg/edge/api/services/channels

     1  package channels
     2  
     3  import (
     4  	"time"
     5  
     6  	"github.com/google/uuid"
     7  	"gopkg.in/yaml.v3"
     8  )
     9  
    10  const (
    11  	// TODO: ask Lee what values he wants for the DefaultChannelRotationIntervalDuration and DefaultChannelExpireBufferDuration.
    12  	DefaultChannelRotationIntervalDuration = time.Hour * 24 * 365
    13  	DefaultChannelExpireBufferDuration     = time.Hour * 24 * 7
    14  	MagPieNamespace                        = "magpie"
    15  )
    16  
    17  // secondStr = fmt.Sprintf("%d", time.Second)
    18  const secondStr = "1000000000"
    19  
    20  // Channel represents a row in the `channels` table.
    21  //
    22  // Channels are configured per environment (dev1, stage1, prod1, etc).
    23  // Channels should never be created, updated, or deleted by anyone other than Edge super admins.
    24  type Channel struct {
    25  	ID uuid.UUID // readonly
    26  
    27  	// Name is unique per environment.
    28  	//
    29  	// It must be less than 46 characters and match the following regular expression `^[a-z]([-]?[a-z0-9])+$`
    30  	// since it is used to construct the bearer token secret's name that is mounted by helm workloads.
    31  	Name string // required
    32  
    33  	Team        string // required
    34  	Description string // required
    35  
    36  	// ExpireBufferDuration is added to a ChannelKeyVersion's RotateAt time to calculate its ExpireAt time.
    37  	ExpireBufferDuration time.Duration // optional (default: DefaultChannelExpireBufferDuration)
    38  
    39  	// RotationIntervalDuration is added to a ChannelKeyVersion's CreatedAt time to calculate its RotateAt time.
    40  	RotationIntervalDuration time.Duration // optional (default: DefaultChannelRotationIntervalDuration)
    41  
    42  	CreatedAt time.Time // readonly
    43  }
    44  
    45  func (c *Channel) setDefaults() {
    46  	if c.ExpireBufferDuration == 0 {
    47  		c.ExpireBufferDuration = DefaultChannelExpireBufferDuration
    48  	}
    49  
    50  	if c.RotationIntervalDuration == 0 {
    51  		c.RotationIntervalDuration = DefaultChannelRotationIntervalDuration
    52  	}
    53  }
    54  
    55  // ChannelUpdateRequest updates a channel's columns using the provided fields.
    56  // Fields set to `nil` are ignored.
    57  type ChannelUpdateRequest struct {
    58  	Team                     *string
    59  	Description              *string
    60  	ExpireBufferDuration     *time.Duration
    61  	RotationIntervalDuration *time.Duration
    62  	// NOTE:
    63  	// Never update the `name` or `channel_id` in the database.
    64  	// Those columns are passed into immutable fields within GCP resources.
    65  }
    66  
    67  // ChannelKeyVersion represents a row in the `channels_key_versions` table.
    68  //
    69  // TODO: confirm with Luc that the Version & SecretManagerLink descriptions are correct.
    70  // IDK if the source of truth is derived from SecretManager resources or KMS resources.
    71  //
    72  // TODO: check with Luc to see if we need to make any additions or changes to the ChannelKeyVersion type.
    73  // Lets confirm/deny this sooner rather than later.
    74  //
    75  // I have a feeling that we will need additional fields for ChannelKeyVersion, and that Version is not enough.
    76  // Are we future proof with just the Version?
    77  // Should we store metadata related the sparrow/magpie services' API like their major version, or the API's path?
    78  //
    79  // I also feel like the SecretManagerLink should be constructed by controllers instead of being stored in the database.
    80  // What happens if the underlying resource ID changes, or the project number changes?
    81  // These things happen rarely, but they do happen, especially changes to resource IDs for resources with immutable fields.
    82  // Will we need a service method to update the SecretManagerLink, or would that be moot if we constructed it in controllers?
    83  type ChannelKeyVersion struct {
    84  	ID uuid.UUID // readonly
    85  
    86  	ChannelID    uuid.UUID // required
    87  	BannerEdgeID uuid.UUID // required
    88  
    89  	// Version is the SecretManagerSecretVersion's `.status.version` value.
    90  	// Version is a serial number that begins at `1`.
    91  	Version int // required
    92  
    93  	// SecretManagerLink is the SecretManagerSecret's external reference.
    94  	//
    95  	// Format: "projects/{{gcp_foreman_project_number}}/secrets/{{secret_manager_secret_resource_id}}"
    96  	SecretManagerLink string // required
    97  
    98  	// RotateAt is when the latest ChannelKeyVersion should be rotated.
    99  	// RotateAt is the zero time if the ChannelKeyVersions has already been rotated.
   100  	//
   101  	// When the channel service's `RotateChannelNow()` method is called, it sets the latest ChannelKeyVersion's RotateAt time to the current time, which marks it for rotation.
   102  	// The channel service's `CreateChannelKeyVersion()` method actually rotates a channel.
   103  	// When a new ChannelKeyVersion is created, the old ChannelKeyVersion's RotateAt time is set to null in the database.
   104  	RotateAt time.Time // readonly
   105  
   106  	// ExpireAt is when data encrypted using this ChannelKeyVersion will no longer be decrypted by magpie.
   107  	//
   108  	// ExpireAt is calculated when a ChannelKeyVersion is created, by adding the Channel's RotationIntervalDuration + ExpireBufferDuration to the CreatedAt time.
   109  	// When a ChannelKeyVersion is actually rotated, the ExpireAt time is recalculated by adding the channel's ExpireBufferDuration to the current time.
   110  	ExpireAt time.Time // readonly
   111  
   112  	CreatedAt time.Time // readonly
   113  }
   114  
   115  func (ckv *ChannelKeyVersion) IsExpired() bool {
   116  	return ckv.ExpireAt.Before(time.Now().UTC())
   117  }
   118  
   119  func (ckv *ChannelKeyVersion) IsLatest() bool {
   120  	return !ckv.RotateAt.IsZero()
   121  }
   122  
   123  // ShouldRotate returns true if the RotateAt time has elapsed.
   124  // This method may only be used with the latest ChannelKeyVersion.
   125  //
   126  // NOTE: ShouldRotate panics if the ChannelKeyVersion has already been rotated.
   127  func (ckv *ChannelKeyVersion) ShouldRotate() bool {
   128  	if !ckv.IsLatest() {
   129  		// Panicking is valid in this situation.
   130  		// Checking `ShouldRotate` on a ChannelKeyVersion that has already been rotated is a logic error.
   131  		// Therefore, instead of returning false, or returning an error that will be logged and ignored, this function panics if `IsLatest()` returns false.
   132  		return false
   133  	}
   134  	return ckv.RotateAt.Before(time.Now().UTC())
   135  }
   136  
   137  // BannerChannel is a composite type that combines:
   138  // - the `Channel` being used by this banner.
   139  // - a slice of `uuid.UUID` containing every `helm_edge_id` mapped to the channel, that is owned by this banner.
   140  // - a slice of `ChannelKeyVersion` containing every key version for the channel, that is owned by this banner.
   141  //
   142  // TODO: ask team if any additional methods should be added to BannerChannel type.
   143  type BannerChannel struct {
   144  	Channel
   145  
   146  	// The `LatestKeyVersion()`, `UnexpiredKeyVersions()`, and `ExpiredKeyVersions()` methods are provided for convenience.
   147  	KeyVersions []ChannelKeyVersion
   148  	HelmEdgeIDs []uuid.UUID
   149  }
   150  
   151  // LatestKeyVersion returns the channel's most recently created ChannelKeyVersion for this banner.
   152  func (bc *BannerChannel) LatestKeyVersion() (latest ChannelKeyVersion, exists bool) {
   153  	for _, ckv := range bc.KeyVersions {
   154  		if ckv.IsLatest() {
   155  			return ckv, true
   156  		}
   157  	}
   158  	return ChannelKeyVersion{}, false
   159  }
   160  
   161  // UnexpiredKeyVersions are the ChannelKeyVersions that can be used for decryption by magpie.
   162  func (bc *BannerChannel) UnexpiredKeyVersions() []ChannelKeyVersion {
   163  	var unexpired []ChannelKeyVersion
   164  	for _, ckv := range bc.KeyVersions {
   165  		if !ckv.IsExpired() {
   166  			unexpired = append(unexpired, ckv)
   167  		}
   168  	}
   169  	return unexpired
   170  }
   171  
   172  // ExpiredKeyVersions need to be cleaned up.
   173  func (bc *BannerChannel) ExpiredKeyVersions() []ChannelKeyVersion {
   174  	var expired []ChannelKeyVersion
   175  	for _, ckv := range bc.KeyVersions {
   176  		if ckv.IsExpired() {
   177  			expired = append(expired, ckv)
   178  		}
   179  	}
   180  	return expired
   181  }
   182  
   183  // ShouldBumpChannelKeyVersion encapsulates the Bannerctl logic that determines when the ChannelKeyVersion should be rotated, created for the first time, or be allowed to expire.
   184  // When this function returns `true`, bannerctl needs to generate a new encryption/decryption key pair, and then call `CreateChannelKeyVersion` in the channel service.
   185  //
   186  // This function returns `false` when none of the banner's helm workloads are using this channel.
   187  // For unused channels, bannerctl should allow the existing key versions to expire instead of rotating them.
   188  // When an unused channel's key versions eventually expire, all of its banner-specific resources should be cleaned up, so that Edge isn't wasting money on unused secret manager secrets & k8s resources.
   189  //
   190  // If any of the banner's helm workloads are using this channel, ShouldBumpKeyVersion returns true when:
   191  // - no channel key version exists for this banner.
   192  // - the latest channel key version's RotateAt time has elapsed.
   193  func (bc *BannerChannel) ShouldBumpKeyVersion() bool {
   194  	latest, exists := bc.LatestKeyVersion()
   195  	rotate := latest.ShouldRotate()
   196  	return exists && rotate
   197  }
   198  
   199  // IsUsedByHelmWorkloads returns true if any the banner's helm workloads are using the channel.
   200  func (bc *BannerChannel) IsUsedByHelmWorkloads() bool {
   201  	return len(bc.HelmEdgeIDs) > 0
   202  }
   203  
   204  type HelmConfigChannel struct {
   205  	Name string `yaml:"name"`
   206  }
   207  
   208  type ParsedHelmConfigChannels struct {
   209  	Channels []HelmConfigChannel `yaml:"channels,omitempty"`
   210  }
   211  
   212  func ParseHelmConfigChannels(configYaml string) (*ParsedHelmConfigChannels, error) {
   213  	var config ParsedHelmConfigChannels
   214  	if err := yaml.Unmarshal([]byte(configYaml), &config); err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	if err := config.validateParsed(); err != nil {
   219  		return nil, err
   220  	}
   221  	return &config, nil
   222  }
   223  
   224  func (config *ParsedHelmConfigChannels) HasChannels() bool {
   225  	return len(config.Channels) > 0
   226  }
   227  
   228  func (config *ParsedHelmConfigChannels) Names() []string {
   229  	var names []string
   230  	for _, c := range config.Channels {
   231  		names = append(names, c.Name)
   232  	}
   233  	return names
   234  }
   235  

View as plain text