Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ manager = str(manager).replace('--provider=openconfig', '--provider={}'.format(p
k8s_yaml(blob(manager))
k8s_resource('network-operator-controller-manager', resource_deps=['controller-gen'], labels=['operator'])

k8s_resource('minio', port_forwards=['9001:9001'])

# Sample resources with manual trigger mode
def device_yaml():
decoded = read_yaml_stream('./config/samples/v1alpha1_device.yaml')
Expand Down Expand Up @@ -170,6 +172,7 @@ k8s_resource(new_name='aaa', objects=['aaa-tacacs:aaa', 'tacacs-server-keys:secr
k8s_yaml('./config/samples/v1alpha1_configbackup.yaml')
k8s_resource(new_name='local-backup', objects=['local-backup:configbackup'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
k8s_resource(new_name='startup-backup', objects=['startup-backup:configbackup'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
k8s_resource(new_name='remote-backup', objects=['remote-backup:configbackup', 'minio-credentials:secret', 'backup-encryption-key:secret'], resource_deps=['minio'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])

k8s_yaml('./config/samples/v1alpha1_indexpool.yaml')
k8s_resource(new_name='indexpool', objects=['indexpool-sample:indexpool'], trigger_mode=TRIGGER_MODE_MANUAL, auto_init=False, labels=['samples'])
Expand Down
100 changes: 90 additions & 10 deletions api/core/v1alpha1/configbackup_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
package v1alpha1

import (
"fmt"
"path"
"sync"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -16,8 +14,10 @@ import (
// ConfigBackupSpec defines the desired state of ConfigBackup.
// +kubebuilder:validation:XValidation:rule="self.type != 'Startup' || (!has(self.path) || size(self.path) == 0)",message="path must be omitted for Startup backups"
// +kubebuilder:validation:XValidation:rule="self.type != 'Local' || (has(self.path) && size(self.path) > 0)",message="path must be set for Local backups"
// +kubebuilder:validation:XValidation:rule="self.type == 'Local' || !has(self.retention)",message="retention must only be specified for Local backups"
// +kubebuilder:validation:XValidation:rule="self.type == 'Local' || self.type == 'Remote' || !has(self.retention)",message="retention must only be specified for Local or Remote backups"
// +kubebuilder:validation:XValidation:rule="self.type == 'Local' || !has(self.storageThreshold)",message="storageThreshold must only be specified for Local backups"
// +kubebuilder:validation:XValidation:rule="self.type != 'Remote' || has(self.s3)",message="s3 must be specified for Remote backups"
// +kubebuilder:validation:XValidation:rule="self.type == 'Remote' || !has(self.s3)",message="s3 must only be specified for Remote backups"
type ConfigBackupSpec struct {
// DeviceRef is a reference to the Device this object belongs to. The Device object must exist in the same namespace.
// Immutable.
Expand Down Expand Up @@ -53,17 +53,23 @@ type ConfigBackupSpec struct {
// StorageThreshold defines the minimum free space that must remain before creating a new Local backup.
// +optional
StorageThreshold *ConfigBackupStorageThreshold `json:"storageThreshold,omitempty"`

// S3 configures the S3-compatible object storage destination for Remote backups.
// +optional
S3 *ConfigBackupS3 `json:"s3,omitempty"`
}

// ConfigBackupType defines how the device should persist a configuration backup.
// +kubebuilder:validation:Enum=Local;Startup
// +kubebuilder:validation:Enum=Local;Startup;Remote
type ConfigBackupType string

const (
// ConfigBackupTypeLocal stores the running configuration in a device-local file path.
ConfigBackupTypeLocal ConfigBackupType = "Local"
// ConfigBackupTypeStartup stores the running configuration as the device startup configuration.
ConfigBackupTypeStartup ConfigBackupType = "Startup"
// ConfigBackupTypeRemote uploads the running configuration to an S3-compatible object store.
ConfigBackupTypeRemote ConfigBackupType = "Remote"
)

// ConfigBackupRetention defines how many historical backups are kept on the device.
Expand Down Expand Up @@ -91,6 +97,58 @@ type ConfigBackupStorageThreshold struct {
MinFreePercent *int32 `json:"minFreePercent,omitempty"`
}

// ConfigBackupS3 configures the S3-compatible object storage destination for Remote backups.
type ConfigBackupS3 struct {
// Endpoint is the S3-compatible endpoint URL (e.g., "https://s3.eu-central-1.amazonaws.com").
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=2048
// +kubebuilder:validation:XValidation:rule="self.startsWith('https://') || self.startsWith('http://')",message="endpoint must be a valid URL starting with http:// or https://"
Endpoint string `json:"endpoint"`

// Bucket is the name of the S3 bucket.
// +required
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
Bucket string `json:"bucket"`

// Region is the endpoint region. Optional for S3-compatible stores that don't require it.
// +optional
// +kubebuilder:validation:MaxLength=63
Region string `json:"region,omitempty"`

// CredentialsSecretRef references a Secret containing "accessKeyID" and "secretAccessKey" keys.
// +required
CredentialsSecretRef SecretReference `json:"credentialsSecretRef"`

// Encryption configures optional encryption for backup objects, performed in the controller pod before upload.
// If omitted, backups are stored unencrypted.
// +optional
Encryption *ConfigBackupEncryption `json:"encryption,omitempty"`
}

// EncryptionAlgorithm defines the supported encryption algorithms for remote backups.
// +kubebuilder:validation:Enum="AES-256-GCM";"ChaCha20-Poly1305"
type EncryptionAlgorithm string

const (
// EncryptionAES256GCM uses AES-256 in GCM mode. Key must be 32 bytes.
EncryptionAES256GCM EncryptionAlgorithm = "AES-256-GCM"
// EncryptionChaCha20Poly1305 uses ChaCha20-Poly1305. Key must be 32 bytes.
EncryptionChaCha20Poly1305 EncryptionAlgorithm = "ChaCha20-Poly1305"
)

// ConfigBackupEncryption configures encryption for remote backup objects, performed in the controller pod.
type ConfigBackupEncryption struct {
// Algorithm is the encryption algorithm to use.
// +required
Algorithm EncryptionAlgorithm `json:"algorithm"`

// KeySecret references the Secret and key containing the 32-byte encryption key.
// +required
KeySecret SecretKeySelector `json:"keySecret"`
}

// ConfigBackupStatus defines the observed state of ConfigBackup.
type ConfigBackupStatus struct {
// Conditions represent the current state of the ConfigBackup resource.
Expand Down Expand Up @@ -164,6 +222,16 @@ type ConfigBackupRunStatus struct {
// +optional
// +kubebuilder:validation:MinLength=1
Filepath string `json:"filepath,omitempty"`

// EncryptionAlgorithm is the encryption algorithm used for this backup, if any.
// Only set for encrypted Remote backups.
// +optional
EncryptionAlgorithm EncryptionAlgorithm `json:"encryptionAlgorithm,omitempty"`

// EncryptionKeySecret is the name of the Secret that provided the encryption key.
// Only set for encrypted Remote backups.
// +optional
EncryptionKeySecret string `json:"encryptionKeySecret,omitempty"`
}

// ConfigBackupStorageStatus contains storage utilization for the configured backup target.
Expand Down Expand Up @@ -217,12 +285,6 @@ type ConfigBackup struct {
Status ConfigBackupStatus `json:"status,omitzero"`
}

// Filename returns a string that can be used as a prefix for backup filenames,
// incorporating the namespace and name of the ConfigBackup resource.
func (c *ConfigBackup) Filename() string {
return path.Join(c.Spec.Path, fmt.Sprintf("configbackup-%s-%s-", c.Namespace, c.Name))
}

// GetConditions implements conditions.Getter.
func (c *ConfigBackup) GetConditions() []metav1.Condition {
return c.Status.Conditions
Expand All @@ -233,6 +295,24 @@ func (c *ConfigBackup) SetConditions(conditions []metav1.Condition) {
c.Status.Conditions = conditions
}

// GetSecretRefs returns the list of SecretReferences used by this ConfigBackup.
// Namespaces are defaulted to the ConfigBackup's namespace if not explicitly set.
func (c *ConfigBackup) GetSecretRefs() []SecretReference {
refs := []SecretReference{}
if c.Spec.S3 != nil {
refs = append(refs, c.Spec.S3.CredentialsSecretRef)
if c.Spec.S3.Encryption != nil {
refs = append(refs, c.Spec.S3.Encryption.KeySecret.SecretReference)
}
}
for i := range refs {
if refs[i].Namespace == "" {
refs[i].Namespace = c.Namespace
}
}
return refs
}

// +kubebuilder:object:root=true

// ConfigBackupList contains a list of ConfigBackup.
Expand Down
10 changes: 10 additions & 0 deletions api/core/v1alpha1/groupversion_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ const (
// This condition is set to True when the controller successfully connects to
// the device, and False when the connection attempt fails.
ReachableCondition = "Reachable"

// RemoteEndpointReadyCondition indicates whether the remote object storage
// endpoint is reachable and the configured bucket exists.
RemoteEndpointReadyCondition = "RemoteEndpointReady"
)

// Reasons that are used across different objects.
Expand Down Expand Up @@ -249,6 +253,12 @@ const (
const (
// PrefixSetNotFoundReason indicates that a referenced PrefixSet was not found.
PrefixSetNotFoundReason = "PrefixSetNotFound"
// SecretNotFoundReason indicates that a referenced Secret was not found.
SecretNotFoundReason = "SecretNotFound"
// RemoteEndpointUnreachableReason indicates that the remote object storage endpoint is not reachable.
RemoteEndpointUnreachableReason = "RemoteEndpointUnreachable"
// EncryptionFailedReason indicates that encryption of the backup data failed.
EncryptionFailedReason = "EncryptionFailed"
)

// Reasons that are specific to [BGPPeer] objects.
Expand Down
42 changes: 42 additions & 0 deletions api/core/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading