package provision import ( "crypto" "crypto/x509" "fmt" "path/filepath" "github.com/spf13/afero" "edge-infra.dev/pkg/lib/crypto/certs/pem" edgex509 "edge-infra.dev/pkg/lib/crypto/certs/x509" "edge-infra.dev/pkg/sds/etcd/operator/internal/resources" "edge-infra.dev/pkg/sds/etcd/operator/internal/tar" "edge-infra.dev/pkg/sds/lib/etcd/client" "edge-infra.dev/pkg/sds/lib/etcd/server" ) // secretContent creates the tarball containing the required certificates for etcd func (r *Reconciler) secretContent(handlers *Handlers) ([]byte, error) { ip := handlers.member.EtcdMember.Spec.Address.Host certList := []edgex509.CertInfo{ server.CertInfo(handlers.member.EtcdMember.Name, ip), server.PeerCertInfo(handlers.member.EtcdMember.Name, ip), client.HealthcheckCertInfo(), client.APIServerEtcdCertInfo(), } requiredFiles, err := r.requiredFiles(r.Fs, certList, handlers) if err != nil { return nil, err } writer := tar.NewWriter() defer writer.Close() // write the required files to the tarball if err := writer.Archive(requiredFiles); err != nil { return nil, fmt.Errorf("failed to write files to tarball: %w", err) } return writer.Bytes(), nil } // requiredFiles creates the list of files that must be copied from the controlplane to // the worker node that the EtcdMember is running on. func (r *Reconciler) requiredFiles(fs afero.Fs, certList []edgex509.CertInfo, handlers *Handlers) ([]tar.File, error) { var requiredFiles []tar.File requiredEtcdCerts, err := r.requiredEtcdCerts(fs, certList) if err != nil { return nil, err } requiredFiles = append(requiredFiles, requiredEtcdCerts...) requiredKubeFiles, err := r.requiredKubeFiles(fs, handlers.member.EtcdMember.Spec.RequiredFiles) if err != nil { return nil, err } return append(requiredFiles, requiredKubeFiles...), nil } // requiredEtcdCerts creates the list of etcd certs that must be copied from the controlplane // to the worker node that the EtcdMember is running on func (r *Reconciler) requiredEtcdCerts(fs afero.Fs, certInfoList []edgex509.CertInfo) ([]tar.File, error) { caCert, err := etcdCaCert(fs) if err != nil { return nil, fmt.Errorf("failed to read cert: %s: %w", caCertPath, err) } caKeySigner, err := etcdCaKeySigner(fs) if err != nil { return nil, fmt.Errorf("failed to decode cert: %s: %w", caCertPath, err) } var certs []tar.File // Create the list of etcd key and cert pairs for _, certInfo := range certInfoList { certs, err = appendKeyCertPair(certs, certInfo, caCert, caKeySigner) if err != nil { return nil, err } } return certs, nil } // etcdCaCert reads the etcd CA cert from the filesystem func etcdCaCert(fs afero.Fs) (*x509.Certificate, error) { certBytes, err := afero.ReadFile(fs, caCertPath) if err != nil { return nil, fmt.Errorf("failed to read cert: %s: %w", caCertPath, err) } caCert, err := pem.GetCertFromPemFile(certBytes) if err != nil { return nil, fmt.Errorf("failed to decode cert: %s: %w", caCertPath, err) } return caCert, nil } // etcdCaKeySigner gets the etcd CA key from the filesystem and returns a signer func etcdCaKeySigner(fs afero.Fs) (crypto.Signer, error) { keyBytes, err := afero.ReadFile(fs, caKeyPath) if err != nil { return nil, fmt.Errorf("failed to read key: %s: %w", caKeyPath, err) } caKeySigner, err := pem.GetKeySignerFromPemFile(keyBytes) if err != nil { return nil, fmt.Errorf("failed to decode key: %s: %w", caKeyPath, err) } return caKeySigner, nil } // appendKeyCertPair appends the key and cert to the list of certs func appendKeyCertPair(certs []tar.File, certInfo edgex509.CertInfo, caCert *x509.Certificate, caKeySigner crypto.Signer) ([]tar.File, error) { encodedKeyPair, err := edgex509.GenerateCertAndKey(certInfo, caCert, caKeySigner) if err != nil { return nil, fmt.Errorf("failed to create x509 public certificate: %w", err) } var path string switch certInfo.Name { case "apiserver-etcd-client": path = "/etc/kubernetes/pki/" default: path = "/etc/kubernetes/pki/etcd/" } key := tar.File{ Name: filepath.Join(path, certInfo.Name+".key"), Bytes: encodedKeyPair.Key, Size: int64(len(encodedKeyPair.Key)), Mode: 0600, } cert := tar.File{ Name: filepath.Join(path, certInfo.Name+".crt"), Bytes: encodedKeyPair.Cert, Size: int64(len(encodedKeyPair.Cert)), Mode: 0644, } return append(certs, key, cert), nil } // requiredKubeFiles creates the list of kubernetes files that must be copied from the // controlplane to the worker node that the EtcdMember is running on. func (r *Reconciler) requiredKubeFiles(fs afero.Fs, fileNames []string) ([]tar.File, error) { var requiredFiles []tar.File for _, fileName := range fileNames { file, err := fs.Open(fileName) if err != nil { return nil, fmt.Errorf("failed to open file: %s: %w", fileName, err) } bytes, err := afero.ReadFile(fs, fileName) if err != nil { return nil, fmt.Errorf("failed to read file: %s: %w", fileName, err) } fileInfo, err := file.Stat() if err != nil { return nil, fmt.Errorf("failed to stat file: %s: %w", fileName, err) } requiredFiles = append(requiredFiles, tar.File{ Name: fileName, Bytes: bytes, Size: fileInfo.Size(), Mode: fileInfo.Mode(), }) } return requiredFiles, nil } // generateSecret generates a new Secret object for the EtcdMember func (r *Reconciler) generateSecret(data []byte, handlers *Handlers) { b := resources.NewSecretHandlerBuilder(). WithClient(r.KubeRetryClient). WithKey(handlers.secret.Key). HandlesSecret(). Named(handlers.member.EtcdMember.Name). InNamespace(operatorNamespace). WithOwner(handlers.member.EtcdMember). WithData(data) handlers.secret = b.Build() }