From d3e7c738ecb8b8acf611e424e81a5bb3341d60a9 Mon Sep 17 00:00:00 2001 From: James Lu Date: Thu, 28 Sep 2023 17:28:21 +0800 Subject: [PATCH] feat(backup): secret HTTP apis Add secret HTTP apis: "SecretCreate", "SecretGet", "SecretList", "SecretUpdate" and "SecretDelete". Ref: 5411 Signed-off-by: James Lu --- api/model.go | 64 ++++++++++++++++++++ api/router.go | 6 ++ api/secret.go | 128 ++++++++++++++++++++++++++++++++++++++++ datastore/kubernetes.go | 25 ++++++++ manager/secret.go | 102 ++++++++++++++++++++++++++++++++ 5 files changed, 325 insertions(+) create mode 100644 api/secret.go create mode 100644 manager/secret.go diff --git a/api/model.go b/api/model.go index 57f818f0c5..e70ed68fde 100644 --- a/api/model.go +++ b/api/model.go @@ -1,6 +1,7 @@ package api import ( + "encoding/base64" "fmt" "strconv" "time" @@ -119,6 +120,14 @@ type SnapshotCR struct { Checksum string `json:"checksum"` } +type SecretInput struct { + client.Resource + + Name string `json:"name"` + SecretType string `json:"secretType"` + Data map[string]string `json:"data"` +} + type BackupTarget struct { client.Resource engineapi.BackupTarget @@ -615,6 +624,7 @@ func NewSchema() *client.Schemas { snapshotCRSchema(schemas.AddType("snapshotCR", SnapshotCR{})) backupVolumeSchema(schemas.AddType("backupVolume", BackupVolume{})) backupTargetSchema(schemas.AddType("backupTarget", BackupTarget{})) + secretSchema(schemas.AddType("secret", SecretInput{})) settingSchema(schemas.AddType("setting", Setting{})) recurringJobSchema(schemas.AddType("recurringJob", RecurringJob{})) engineImageSchema(schemas.AddType("engineImage", EngineImage{})) @@ -791,6 +801,28 @@ func backupVolumeSchema(backupVolume *client.Schema) { } } +func secretSchema(secret *client.Schema) { + secret.CollectionMethods = []string{"GET", "POST"} + secret.ResourceMethods = []string{"GET", "PUT", "DELETE"} + + name := secret.ResourceFields["name"] + name.Required = true + name.Unique = true + name.Create = true + secret.ResourceFields["name"] = name + + secretType := secret.ResourceFields["type"] + secretType.Required = true + secretType.Unique = false + secretType.Create = true + secret.ResourceFields["type"] = secretType + + data := secret.ResourceFields["data"] + data.Type = "map[string]" + data.Nullable = true + secret.ResourceFields["data"] = data +} + func backupTargetSchema(backupTarget *client.Schema) { backupTarget.CollectionMethods = []string{"GET", "POST"} backupTarget.ResourceMethods = []string{"GET", "PUT", "DELETE"} @@ -1680,6 +1712,38 @@ func toBackupTargetResource(bt *longhorn.BackupTarget, apiContext *api.ApiContex return res } +func toSecretResource(s *corev1.Secret, apiContext *api.ApiContext) *SecretInput { + if s == nil { + return nil + } + + dataMap := make(map[string]string, len(s.Data)) + for key, data := range s.Data { + dataMap[key] = base64.StdEncoding.EncodeToString(data) + } + + res := &SecretInput{ + Resource: client.Resource{ + Id: s.Name, + Type: "secret", + Links: map[string]string{}, + }, + + Name: s.Name, + SecretType: string(s.Type), + Data: dataMap, + } + return res +} + +func toSecretCollection(ss []*corev1.Secret, apiContext *api.ApiContext) *client.GenericCollection { + data := []interface{}{} + for _, s := range ss { + data = append(data, toSecretResource(s, apiContext)) + } + return &client.GenericCollection{Data: data, Collection: client.Collection{ResourceType: "secret"}} +} + func toBackupVolumeResource(bv *longhorn.BackupVolume, apiContext *api.ApiContext) *BackupVolume { if bv == nil { return nil diff --git a/api/router.go b/api/router.go index bde3a5cbc3..209b949f3f 100644 --- a/api/router.go +++ b/api/router.go @@ -117,6 +117,12 @@ func NewRouter(s *Server) *mux.Router { r.Methods("POST").Path("/v1/volumes/{name}").Queries("action", name).Handler(f(schemas, action)) } + r.Methods("GET").Path("/v1/secrets").Handler(f(schemas, s.SecretList)) + r.Methods("GET").Path("/v1/secrets/{name}").Handler(f(schemas, s.SecretGet)) + r.Methods("POST").Path("/v1/secrets").Handler(f(schemas, s.SecretCreate)) + r.Methods("DELETE").Path("/v1/secrets/{name}").Handler(f(schemas, s.SecretDelete)) + r.Methods("PUT").Path("/v1/secrets/{name}").Handler(f(schemas, s.SecretUpdate)) + r.Methods("GET").Path("/v1/backuptargets").Handler(f(schemas, s.BackupTargetList)) r.Methods("GET").Path("/v1/backuptargets/{name}").Handler(f(schemas, s.BackupTargetGet)) r.Methods("POST").Path("/v1/backuptargets").Handler(f(schemas, s.BackupTargetCreate)) diff --git a/api/secret.go b/api/secret.go new file mode 100644 index 0000000000..0caecb2087 --- /dev/null +++ b/api/secret.go @@ -0,0 +1,128 @@ +package api + +import ( + "encoding/base64" + "fmt" + "net/http" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/rancher/go-rancher/api" + "github.com/rancher/go-rancher/client" + + "github.com/longhorn/longhorn-manager/util" +) + +func (s *Server) SecretList(rw http.ResponseWriter, req *http.Request) (err error) { + apiContext := api.GetApiContext(req) + + secrets, err := s.secretList(apiContext) + if err != nil { + return err + } + apiContext.Write(secrets) + return nil +} + +func (s *Server) secretList(apiContext *api.ApiContext) (*client.GenericCollection, error) { + list, err := s.m.ListSecretsSorted() + if err != nil { + return nil, errors.Wrap(err, "failed to list secrets") + } + return toSecretCollection(list, apiContext), nil +} + +func (s *Server) SecretGet(rw http.ResponseWriter, req *http.Request) error { + apiContext := api.GetApiContext(req) + + secretName := mux.Vars(req)["name"] + + secret, err := s.m.GetSecret(secretName) + if err != nil { + return errors.Wrapf(err, "failed to get secret '%s'", secretName) + } + apiContext.Write(toSecretResource(secret, apiContext)) + return nil +} + +func (s *Server) SecretCreate(rw http.ResponseWriter, req *http.Request) error { + var input SecretInput + apiContext := api.GetApiContext(req) + + if err := apiContext.Read(&input); err != nil { + return err + } + + secretData := make(map[string][]byte, len(input.Data)) + for key, data := range input.Data { + decodeData, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return err + } + secretData[key] = decodeData + } + + obj, err := s.m.CreateSecret(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: input.Name, + }, + Data: secretData, + Type: corev1.SecretType(input.Type), + }) + if err != nil { + return errors.Wrapf(err, "failed to create secret %v", input.Name) + } + apiContext.Write(toSecretResource(obj, apiContext)) + return nil +} + +func (s *Server) SecretUpdate(rw http.ResponseWriter, req *http.Request) error { + var input SecretInput + + apiContext := api.GetApiContext(req) + if err := apiContext.Read(&input); err != nil { + return err + } + + secretName := mux.Vars(req)["name"] + secretData := make(map[string][]byte, len(input.Data)) + for key, data := range input.Data { + decodeData, err := base64.StdEncoding.DecodeString(data) + if err != nil { + return err + } + secretData[key] = decodeData + } + obj, err := util.RetryOnConflictCause(func() (interface{}, error) { + return s.m.UpdateSecret(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + }, + Data: secretData, + Type: corev1.SecretType(input.Type), + }) + }) + if err != nil { + return err + } + secret, ok := obj.(*corev1.Secret) + if !ok { + return fmt.Errorf("failed to convert %v to secret object", secretName) + } + + apiContext.Write(toSecretResource(secret, apiContext)) + return nil +} + +func (s *Server) SecretDelete(rw http.ResponseWriter, req *http.Request) error { + secretName := mux.Vars(req)["name"] + if err := s.m.DeleteSecret(secretName); err != nil { + return errors.Wrapf(err, "failed to delete secret %v", secretName) + } + + return nil +} diff --git a/datastore/kubernetes.go b/datastore/kubernetes.go index e28fd9dd83..d0c81e4cee 100644 --- a/datastore/kubernetes.go +++ b/datastore/kubernetes.go @@ -651,6 +651,31 @@ func (s *DataStore) DeleteConfigMap(namespace, name string) error { return nil } +// CreateSecret creates a Secret resource +func (s *DataStore) CreateSecret(namespace string, secret *corev1.Secret) (*corev1.Secret, error) { + return s.kubeClient.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) +} + +// UpdateSecret updates a Secret resource +func (s *DataStore) UpdateSecret(namespace string, secret *corev1.Secret) (*corev1.Secret, error) { + return s.kubeClient.CoreV1().Secrets(namespace).Update(context.TODO(), secret, metav1.UpdateOptions{}) +} + +// DeleteSecret deletes a Secret resource +func (s *DataStore) DeleteSecret(namespace, name string) error { + return s.kubeClient.CoreV1().Secrets(namespace).Delete(context.TODO(), name, metav1.DeleteOptions{}) +} + +// ListSecretsRO returns an object contains all secrets in Longhorn namespace in the cluster Secret resources +func (s *DataStore) ListSecretsRO(namespace string) ([]*corev1.Secret, error) { + return s.secretLister.Secrets(namespace).List(labels.Everything()) +} + +// ListAllSecretsRO returns an object contains all secrets in all namespaces in the cluster Secret resources +func (s *DataStore) ListAllSecretsRO() ([]*corev1.Secret, error) { + return s.secretLister.List(labels.Everything()) +} + // GetSecretRO gets Secret with the given namespace and name // This function returns direct reference to the internal cache object and should not be mutated. // Consider using this function when you can guarantee read only access and don't want the overhead of deep copies diff --git a/manager/secret.go b/manager/secret.go new file mode 100644 index 0000000000..63576a0580 --- /dev/null +++ b/manager/secret.go @@ -0,0 +1,102 @@ +package manager + +import ( + "reflect" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + corev1 "k8s.io/api/core/v1" + + "github.com/longhorn/longhorn-manager/util" +) + +func (m *VolumeManager) GetSecret(name string) (*corev1.Secret, error) { + namespace, err := m.ds.GetLonghornNamespace() + if err != nil { + return nil, err + } + return m.ds.GetSecretRO(namespace.Name, name) +} + +func (m *VolumeManager) ListSecretsSorted() ([]*corev1.Secret, error) { + namespace, err := m.ds.GetLonghornNamespace() + if err != nil { + return nil, err + } + secretList, err := m.ds.ListSecretsRO(namespace.Name) + if err != nil { + return []*corev1.Secret{}, err + } + + itemMap := map[string]*corev1.Secret{} + for _, itemRO := range secretList { + itemMap[itemRO.Name] = itemRO + } + + secrets := make([]*corev1.Secret, len(secretList)) + sortedSecrets, err := util.SortKeys(itemMap) + if err != nil { + return []*corev1.Secret{}, err + } + for i, name := range sortedSecrets { + secrets[i] = itemMap[name] + } + return secrets, nil +} + +func (m *VolumeManager) CreateSecret(secret *corev1.Secret) (*corev1.Secret, error) { + namespace, err := m.ds.GetLonghornNamespace() + if err != nil { + return nil, err + } + + s, err := m.ds.CreateSecret(namespace.Name, secret) + if err != nil { + return nil, err + } + logrus.Infof("Created secret %v", secret.Name) + return s, nil +} + +func (m *VolumeManager) UpdateSecret(secret *corev1.Secret) (*corev1.Secret, error) { + var err error + defer func() { + err = errors.Wrapf(err, "unable to update %v secret", secret.Name) + }() + namespace, err := m.ds.GetLonghornNamespace() + if err != nil { + return nil, err + } + + existingSecret, err := m.ds.GetSecret(namespace.Name, secret.Name) + if err != nil { + return nil, errors.Wrapf(err, "failed to get secret %v", secret.Name) + } + existingSecretDataSorted, err := util.SortKeys(existingSecret.Data) + if err != nil { + return nil, err + } + secretDataSorted, err := util.SortKeys(secret.Data) + if err != nil { + return nil, err + } + if existingSecret.Type == secret.Type && + reflect.DeepEqual(existingSecretDataSorted, secretDataSorted) { + return secret, nil + } + + return m.ds.UpdateSecret(namespace.Name, secret) +} + +func (m *VolumeManager) DeleteSecret(name string) error { + namespace, err := m.ds.GetLonghornNamespace() + if err != nil { + return err + } + if err := m.ds.DeleteSecret(namespace.Name, name); err != nil { + return err + } + logrus.Infof("Deleted secret %v", name) + return nil +}