diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index adc3df1e7..50cc07e0e 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -7,6 +7,7 @@ import ( "github.com/boltdb/bolt" "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/deploymentkey" "github.com/portainer/portainer/api/bolt/dockerhub" "github.com/portainer/portainer/api/bolt/endpoint" "github.com/portainer/portainer/api/bolt/endpointgroup" @@ -39,6 +40,7 @@ type Store struct { checkForDataMigration bool fileService portainer.FileService RoleService *role.Service + DeploymentKeyService *deploymentkey.Service DockerHubService *dockerhub.Service EndpointGroupService *endpointgroup.Service EndpointService *endpoint.Service @@ -148,6 +150,12 @@ func (store *Store) initServices() error { } store.RoleService = authorizationsetService + deploymentkeyService, err := deploymentkey.NewService(store.db) + if err != nil { + return err + } + store.DeploymentKeyService = deploymentkeyService + dockerhubService, err := dockerhub.NewService(store.db) if err != nil { return err diff --git a/api/bolt/deploymentkey/deploymentkey.go b/api/bolt/deploymentkey/deploymentkey.go new file mode 100644 index 000000000..22177e53b --- /dev/null +++ b/api/bolt/deploymentkey/deploymentkey.go @@ -0,0 +1,120 @@ +package deploymentkey + +import ( + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" + + "github.com/boltdb/bolt" +) + +const ( + // BucketName represents the name of the bucket where this service stores data. + BucketName = "deploymentkey" +) + +// Service represents a service for managing deploymentkey data. +type Service struct { + db *bolt.DB +} + +// NewService creates a new instance of a service. +func NewService(db *bolt.DB) (*Service, error) { + err := internal.CreateBucket(db, BucketName) + if err != nil { + return nil, err + } + + return &Service{ + db: db, + }, nil +} + +// DeploymentKeys return all the deployment keys that are created. +func (service *Service) DeploymentKeys() ([]portainer.DeploymentKey, error) { + var deploymentkeys = make([]portainer.DeploymentKey, 0) + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var deploymentkey portainer.DeploymentKey + err := internal.UnmarshalObject(v, &deploymentkey) + if err != nil { + return err + } + deploymentkeys = append(deploymentkeys, deploymentkey) + } + + return nil + }) + + return deploymentkeys, err +} + +// DeploymentKey returns the deployment key by deployment key ID. +func (service *Service) DeploymentKey(ID portainer.DeploymentKeyID) (*portainer.DeploymentKey, error) { + var deploymentkey portainer.DeploymentKey + identifier := internal.Itob(int(ID)) + + err := internal.GetObject(service.db, BucketName, identifier, &deploymentkey) + if err != nil { + return nil, err + } + + return &deploymentkey, nil +} + +// DeploymentKeyByName returns a deploymentkey by name. +func (service *Service) DeploymentKeyByName(name string) (*portainer.DeploymentKey, error) { + var deploymentkey *portainer.DeploymentKey + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var t portainer.DeploymentKey + err := internal.UnmarshalObject(v, &t) + if err != nil { + return err + } + + if t.Name == name { + deploymentkey = &t + break + } + } + + if deploymentkey == nil { + return portainer.ErrObjectNotFound + } + + return nil + }) + + return deploymentkey, err +} + +// DeleteDeploymentKey deletes a deployment key. +func (service *Service) DeleteDeploymentKey(ID portainer.DeploymentKeyID) error { + identifier := internal.Itob(int(ID)) + return internal.DeleteObject(service.db, BucketName, identifier) +} + +// CreateDeploymentKey creates a deployment key. +func (service *Service) CreateDeploymentKey(deploymentkey *portainer.DeploymentKey) error { + return service.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + id, _ := bucket.NextSequence() + deploymentkey.ID = portainer.DeploymentKeyID(id) + + data, err := internal.MarshalObject(deploymentkey) + if err != nil { + return err + } + + return bucket.Put(internal.Itob(int(deploymentkey.ID)), data) + }) +} diff --git a/api/http/handler/deploymentkeys/deploymentkey_create.go b/api/http/handler/deploymentkeys/deploymentkey_create.go new file mode 100644 index 000000000..2ec7838ce --- /dev/null +++ b/api/http/handler/deploymentkeys/deploymentkey_create.go @@ -0,0 +1,52 @@ +package deploymentkeys + +import ( + "net/http" + + "github.com/asaskevich/govalidator" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" +) + +type deploymentKeyCreatePayload struct { + Name string +} + +func (payload *deploymentKeyCreatePayload) Validate(r *http.Request) error { + if govalidator.IsNull(payload.Name) { + return portainer.Error("Invalid deploymentkey name") + } + return nil +} + +func (handler *Handler) deploymentkeyCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + var payload deploymentKeyCreatePayload + err := request.DecodeAndValidateJSONPayload(r, &payload) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} + } + + deploymentkey, err := handler.DeploymentKeyService.DeploymentKeyByName(payload.Name) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a deployment key with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a deployment key with the specified identifier inside the database", err} + } + + // Add a function to call and create public key and private key + + deploymentkey = &portainer.DeploymentKey{ + Name: payload.Name, + PublicKey: "SHA256:hellotherepublic", + PrivateKey: "SHA256:hellothereprivate", + } + + err = handler.DeploymentKeyService.CreateDeploymentKey(deploymentkey) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the deployment key inside the database", err} + } + + return response.JSON(w, deploymentkey) +} diff --git a/api/http/handler/deploymentkeys/deploymentkey_delete.go b/api/http/handler/deploymentkeys/deploymentkey_delete.go new file mode 100644 index 000000000..63fe21455 --- /dev/null +++ b/api/http/handler/deploymentkeys/deploymentkey_delete.go @@ -0,0 +1,25 @@ +package deploymentkeys + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" +) + +// DELETE request on /api/webhook/:serviceID +func (handler *Handler) deploymentkeyDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + id, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid deployment key id", err} + } + + err = handler.DeploymentKeyService.DeleteDeploymentKey(portainer.DeploymentKeyID(id)) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the deployment key from the database", err} + } + + return response.Empty(w) +} diff --git a/api/http/handler/deploymentkeys/deploymentkey_inspect.go b/api/http/handler/deploymentkeys/deploymentkey_inspect.go new file mode 100644 index 000000000..4cff8fdd3 --- /dev/null +++ b/api/http/handler/deploymentkeys/deploymentkey_inspect.go @@ -0,0 +1,27 @@ +package deploymentkeys + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer/api" +) + +// GET request on /api/deployment_keys/:id +func (handler *Handler) deploymentkeyInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + deploymentkeyID, err := request.RetrieveNumericRouteVariableValue(r, "id") + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid deploymentkey identifier route variable", err} + } + + deploymentkey, err := handler.DeploymentKeyService.DeploymentKey(portainer.DeploymentKeyID(deploymentkeyID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find a deployment key with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a deployment key with the specified identifier inside the database", err} + } + + return response.JSON(w, deploymentkey) +} diff --git a/api/http/handler/deploymentkeys/deploymentkey_list.go b/api/http/handler/deploymentkeys/deploymentkey_list.go new file mode 100644 index 000000000..172ae5a23 --- /dev/null +++ b/api/http/handler/deploymentkeys/deploymentkey_list.go @@ -0,0 +1,18 @@ +package deploymentkeys + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" +) + +// GET request on /api/deployment_keys +func (handler *Handler) deploymentkeyList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + deploymentkeys, err := handler.DeploymentKeyService.DeploymentKeys() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve deploymentkeys from the database", err} + } + + return response.JSON(w, deploymentkeys) +} diff --git a/api/http/handler/deploymentkeys/handler.go b/api/http/handler/deploymentkeys/handler.go new file mode 100644 index 000000000..9fd9b709f --- /dev/null +++ b/api/http/handler/deploymentkeys/handler.go @@ -0,0 +1,36 @@ +package deploymentkeys + +import ( + "net/http" + + "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" +) + +// Handler is the HTTP handler used to handle webhook operations. +type Handler struct { + *mux.Router + DeploymentKeyService portainer.DeploymentKeyService +} + +// NewHandler creates a handler to manage settings operations. +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/deployment_keys", + // bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyCreate))).Methods(http.MethodPost) + bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyCreate))).Methods(http.MethodPost) + h.Handle("/deployment_keys", + // bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyList))).Methods(http.MethodGet) + bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyList))).Methods(http.MethodGet) + h.Handle("/deployment_keys/{id}", + // bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyList))).Methods(http.MethodGet) + bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyInspect))).Methods(http.MethodGet) + h.Handle("/deployment_keys/{id}", + // bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.deploymentkeyDelete))).Methods(http.MethodDelete) + bouncer.PublicAccess(httperror.LoggerHandler(h.deploymentkeyDelete))).Methods(http.MethodDelete) + return h +} diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 9dc541641..da1f34016 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -9,6 +9,7 @@ import ( "github.com/portainer/portainer/api/http/handler/roles" "github.com/portainer/portainer/api/http/handler/auth" + "github.com/portainer/portainer/api/http/handler/deploymentkeys" "github.com/portainer/portainer/api/http/handler/dockerhub" "github.com/portainer/portainer/api/http/handler/endpointgroups" "github.com/portainer/portainer/api/http/handler/endpointproxy" @@ -34,6 +35,7 @@ import ( // Handler is a collection of all the service handlers. type Handler struct { AuthHandler *auth.Handler + DeploymentKeyHandler *deploymentkeys.Handler DockerHubHandler *dockerhub.Handler EndpointGroupHandler *endpointgroups.Handler EndpointHandler *endpoints.Handler @@ -63,6 +65,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case strings.HasPrefix(r.URL.Path, "/api/auth"): http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/deployment_keys"): + http.StripPrefix("/api", h.DeploymentKeyHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/dockerhub"): http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/endpoint_groups"): diff --git a/api/http/proxy/factory_local_windows.go b/api/http/proxy/factory_local_windows.go index a2105f886..71272ce98 100644 --- a/api/http/proxy/factory_local_windows.go +++ b/api/http/proxy/factory_local_windows.go @@ -3,10 +3,10 @@ package proxy import ( + "github.com/Microsoft/go-winio" "net" "net/http" - "github.com/Microsoft/go-winio" - + portainer "github.com/portainer/portainer/api" ) diff --git a/api/portainer.go b/api/portainer.go index 1cb48f826..a3875f7d5 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -216,6 +216,18 @@ type ( TLSConfig TLSConfiguration `json:"TLSConfig"` } + // DeploymentKeyID represents + DeploymentKeyID int + + // DeploymentKey represents the SSH key details that will be used to + // connect to GitHub for deployments based on private key clone + DeploymentKey struct { + ID DeploymentKeyID `json:"Id` + Name string `json:"Name"` + PublicKey string `json:"PublicKey"` + PrivateKey string `json:"PrivateKey"` + } + // DockerHub represents all the required information to connect and use the // Docker Hub DockerHub struct { @@ -674,6 +686,15 @@ type ( GetNextIdentifier() int } + // DeploymentKeyService represents a service for managing the DeploymentKey object + DeploymentKeyService interface { + DeploymentKey(ID DeploymentKeyID) (*DeploymentKey, error) + DeploymentKeyByName(name string) (*DeploymentKey, error) + DeploymentKeys() ([]DeploymentKey, error) + DeleteDeploymentKey(ID DeploymentKeyID) error + CreateDeploymentKey(deploymentkey *DeploymentKey) error + } + // DockerHubService represents a service for managing the DockerHub object DockerHubService interface { DockerHub() (*DockerHub, error) @@ -876,7 +897,7 @@ const ( PortainerAgentSignatureMessage = "Portainer-App" // SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer SupportedDockerAPIVersion = "1.24" - // ExtensionServer represents the server used by Portainer to communicate with extensions + // ExtensionServer represents the server used by Portainer to communicate with extensions ExtensionServer = "localhost" )