Skip to content

Commit

Permalink
feat(backup): secret HTTP apis
Browse files Browse the repository at this point in the history
Add secret HTTP apis: "SecretCreate", "SecretGet", "SecretList",
"SecretUpdate" and "SecretDelete".

Ref: 5411

Signed-off-by: James Lu <james.lu@suse.com>
  • Loading branch information
mantissahz committed Sep 28, 2023
1 parent a5fba34 commit d3e7c73
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 0 deletions.
64 changes: 64 additions & 0 deletions api/model.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package api

import (
"encoding/base64"
"fmt"
"strconv"
"time"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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{}))
Expand Down Expand Up @@ -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"}
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions api/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
128 changes: 128 additions & 0 deletions api/secret.go
Original file line number Diff line number Diff line change
@@ -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
}
25 changes: 25 additions & 0 deletions datastore/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions manager/secret.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit d3e7c73

Please sign in to comment.