mirror of https://github.com/portainer/portainer
chore(fdo): remove FDO code EE-7235 (#11981)
parent
1a3db327c7
commit
19fa40286a
|
@ -1,43 +0,0 @@
|
|||
package fdoprofile
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
)
|
||||
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
const BucketName = "fdo_profiles"
|
||||
|
||||
// Service represents a service for managingFDO Profiles data.
|
||||
type Service struct {
|
||||
dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]
|
||||
}
|
||||
|
||||
// NewService creates a new instance of a service.
|
||||
func NewService(connection portainer.Connection) (*Service, error) {
|
||||
err := connection.SetServiceName(BucketName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Service{
|
||||
BaseDataService: dataservices.BaseDataService[portainer.FDOProfile, portainer.FDOProfileID]{
|
||||
Bucket: BucketName,
|
||||
Connection: connection,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Create assign an ID to a new FDO Profile and saves it.
|
||||
func (service *Service) Create(FDOProfile *portainer.FDOProfile) error {
|
||||
return service.Connection.CreateObjectWithId(
|
||||
BucketName,
|
||||
int(FDOProfile.ID),
|
||||
FDOProfile,
|
||||
)
|
||||
}
|
||||
|
||||
// GetNextIdentifier returns the next identifier for a FDO Profile.
|
||||
func (service *Service) GetNextIdentifier() int {
|
||||
return service.Connection.GetNextIdentifier(BucketName)
|
||||
}
|
|
@ -15,7 +15,6 @@ type (
|
|||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
FDOProfile() FDOProfileService
|
||||
HelmUserRepository() HelmUserRepositoryService
|
||||
Registry() RegistryService
|
||||
ResourceControl() ResourceControlService
|
||||
|
@ -120,12 +119,6 @@ type (
|
|||
BucketName() string
|
||||
}
|
||||
|
||||
// FDOProfileService represents a service to manage FDO Profiles
|
||||
FDOProfileService interface {
|
||||
BaseCRUD[portainer.FDOProfile, portainer.FDOProfileID]
|
||||
GetNextIdentifier() int
|
||||
}
|
||||
|
||||
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
|
||||
HelmUserRepositoryService interface {
|
||||
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
||||
|
|
|
@ -13,7 +13,6 @@ import (
|
|||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||
"github.com/portainer/portainer/api/dataservices/extension"
|
||||
"github.com/portainer/portainer/api/dataservices/fdoprofile"
|
||||
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||
"github.com/portainer/portainer/api/dataservices/registry"
|
||||
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
||||
|
@ -41,7 +40,6 @@ type (
|
|||
endpointService *endpoint.Service
|
||||
endpointRelationService *endpointrelation.Service
|
||||
extensionService *extension.Service
|
||||
fdoProfilesService *fdoprofile.Service
|
||||
registryService *registry.Service
|
||||
resourceControlService *resourcecontrol.Service
|
||||
roleService *role.Service
|
||||
|
@ -69,7 +67,6 @@ type (
|
|||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
FDOProfilesService *fdoprofile.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
RoleService *role.Service
|
||||
|
@ -99,7 +96,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
|||
endpointService: parameters.EndpointService,
|
||||
endpointRelationService: parameters.EndpointRelationService,
|
||||
extensionService: parameters.ExtensionService,
|
||||
fdoProfilesService: parameters.FDOProfilesService,
|
||||
registryService: parameters.RegistryService,
|
||||
resourceControlService: parameters.ResourceControlService,
|
||||
roleService: parameters.RoleService,
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||
"github.com/portainer/portainer/api/dataservices/extension"
|
||||
"github.com/portainer/portainer/api/dataservices/fdoprofile"
|
||||
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
|
||||
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||
"github.com/portainer/portainer/api/dataservices/registry"
|
||||
|
@ -55,7 +54,6 @@ type Store struct {
|
|||
EndpointService *endpoint.Service
|
||||
EndpointRelationService *endpointrelation.Service
|
||||
ExtensionService *extension.Service
|
||||
FDOProfilesService *fdoprofile.Service
|
||||
HelmUserRepositoryService *helmuserrepository.Service
|
||||
RegistryService *registry.Service
|
||||
ResourceControlService *resourcecontrol.Service
|
||||
|
@ -138,12 +136,6 @@ func (store *Store) initServices() error {
|
|||
}
|
||||
store.ExtensionService = extensionService
|
||||
|
||||
fdoProfilesService, err := fdoprofile.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.FDOProfilesService = fdoProfilesService
|
||||
|
||||
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -289,11 +281,6 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
|
|||
return store.EndpointRelationService
|
||||
}
|
||||
|
||||
// FDOProfile gives access to the FDOProfile data management layer
|
||||
func (store *Store) FDOProfile() dataservices.FDOProfileService {
|
||||
return store.FDOProfilesService
|
||||
}
|
||||
|
||||
// HelmUserRepository access the helm user repository settings
|
||||
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
|
||||
return store.HelmUserRepositoryService
|
||||
|
|
|
@ -44,7 +44,6 @@ func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
|
|||
return tx.store.EndpointRelationService.Tx(tx.tx)
|
||||
}
|
||||
|
||||
func (tx *StoreTx) FDOProfile() dataservices.FDOProfileService { return nil }
|
||||
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
|
||||
|
||||
func (tx *StoreTx) Registry() dataservices.RegistryService {
|
||||
|
|
|
@ -648,12 +648,6 @@
|
|||
"TemplatesURL": "",
|
||||
"TrustOnFirstConnect": false,
|
||||
"UserSessionTimeout": "8h",
|
||||
"fdoConfiguration": {
|
||||
"enabled": false,
|
||||
"ownerPassword": "",
|
||||
"ownerURL": "",
|
||||
"ownerUsername": ""
|
||||
},
|
||||
"openAMTConfiguration": {
|
||||
"certFileContent": "",
|
||||
"certFileName": "",
|
||||
|
|
|
@ -36,8 +36,6 @@ const (
|
|||
ManifestFileDefaultName = "k8s-deployment.yml"
|
||||
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
|
||||
EdgeStackStorePath = "edge_stacks"
|
||||
// FDOProfileStorePath represents the subfolder where FDO profiles files are stored in the file store folder.
|
||||
FDOProfileStorePath = "fdo_profiles"
|
||||
// PrivateKeyFile represents the name on disk of the file containing the private key.
|
||||
PrivateKeyFile = "portainer.key"
|
||||
// PublicKeyFile represents the name on disk of the file containing the public key.
|
||||
|
@ -1003,23 +1001,6 @@ func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error
|
|||
return os.Rename(originalPath, newPath)
|
||||
}
|
||||
|
||||
// StoreFDOProfileFileFromBytes creates a subfolder in the FDOProfileStorePath and stores a new file from bytes.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error) {
|
||||
err := service.createDirectoryInStore(FDOProfileStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
filePath := JoinPaths(FDOProfileStorePath, fdoProfileIdentifier)
|
||||
err = service.createFileInStore(filePath, bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return service.wrapFileStore(filePath), nil
|
||||
}
|
||||
|
||||
func CreateFile(path string, r io.Reader) error {
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,247 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/third_party/digest"
|
||||
)
|
||||
|
||||
type FDOOwnerClient struct {
|
||||
OwnerURL string
|
||||
Username string
|
||||
Password string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
type ServiceInfo struct {
|
||||
Module string
|
||||
Var string
|
||||
Filename string
|
||||
Bytes []byte
|
||||
GUID string
|
||||
Device string
|
||||
Priority int
|
||||
OS string
|
||||
Version string
|
||||
Arch string
|
||||
CRID int
|
||||
Hash string
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) doDigestAuthReq(method, endpoint, contentType string, body io.Reader) (*http.Response, error) {
|
||||
transport := digest.NewTransport(c.Username, c.Password)
|
||||
|
||||
client, err := transport.Client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.Timeout = c.Timeout
|
||||
|
||||
e, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u, err := url.Parse(c.OwnerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.ResolveReference(e).String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PostVoucher(ov []byte) (string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPost,
|
||||
"api/v1/owner/vouchers",
|
||||
"application/cbor",
|
||||
bytes.NewReader(ov),
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PutDeviceSVI(info ServiceInfo) error {
|
||||
values := url.Values{}
|
||||
values.Set("module", info.Module)
|
||||
values.Set("var", info.Var)
|
||||
values.Set("filename", info.Filename)
|
||||
values.Set("guid", info.GUID)
|
||||
values.Set("device", info.Device)
|
||||
values.Set("priority", strconv.Itoa(info.Priority))
|
||||
values.Set("os", info.OS)
|
||||
values.Set("version", info.Version)
|
||||
values.Set("arch", info.Arch)
|
||||
values.Set("crid", strconv.Itoa(info.CRID))
|
||||
values.Set("hash", info.Hash)
|
||||
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPut,
|
||||
"api/v1/device/svi?"+values.Encode(),
|
||||
"application/octet-stream",
|
||||
strings.NewReader(string(info.Bytes)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) PutDeviceSVIRaw(info url.Values, body []byte) error {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodPut,
|
||||
"api/v1/device/svi?"+info.Encode(),
|
||||
"application/octet-stream",
|
||||
strings.NewReader(string(body)),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) GetVouchers() ([]string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodGet,
|
||||
"api/v1/owner/vouchers",
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
contents, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
guids := strings.FieldsFunc(
|
||||
strings.TrimSpace(string(contents)),
|
||||
func(c rune) bool {
|
||||
return c == ','
|
||||
},
|
||||
)
|
||||
|
||||
return guids, nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) DeleteVoucher(guid string) error {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodDelete,
|
||||
"api/v1/owner/vouchers?id="+guid,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) GetDeviceSVI(guid string) (string, error) {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodGet,
|
||||
"api/v1/device/svi?guid="+guid,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func (c FDOOwnerClient) DeleteDeviceSVI(id string) error {
|
||||
resp, err := c.doDigestAuthReq(
|
||||
http.MethodDelete,
|
||||
"api/v1/device/svi?id="+id,
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return errors.New(http.StatusText(resp.StatusCode))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -19,7 +19,6 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/gitops"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
|
||||
"github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
|
@ -69,7 +68,6 @@ type Handler struct {
|
|||
SettingsHandler *settings.Handler
|
||||
SSLHandler *ssl.Handler
|
||||
OpenAMTHandler *openamt.Handler
|
||||
FDOHandler *fdo.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StorybookHandler *storybook.Handler
|
||||
SystemHandler *system.Handler
|
||||
|
@ -252,8 +250,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/open_amt"):
|
||||
http.StripPrefix("/api", h.OpenAMTHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/fdo"):
|
||||
http.StripPrefix("/api", h.FDOHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/teams"):
|
||||
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):
|
||||
|
|
|
@ -1,201 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/fxamacker/cbor/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
deploymentScriptName = "fdo.sh"
|
||||
)
|
||||
|
||||
type deviceConfigurePayload struct {
|
||||
EdgeID string `json:"edgeID"`
|
||||
EdgeKey string `json:"edgeKey"`
|
||||
Name string `json:"name"`
|
||||
ProfileID int `json:"profile"`
|
||||
}
|
||||
|
||||
func (payload *deviceConfigurePayload) Validate(r *http.Request) error {
|
||||
if payload.EdgeID == "" {
|
||||
return errors.New("invalid edge ID provided")
|
||||
}
|
||||
|
||||
if payload.EdgeKey == "" {
|
||||
return errors.New("invalid edge key provided")
|
||||
}
|
||||
|
||||
if payload.Name == "" {
|
||||
return errors.New("the device name cannot be empty")
|
||||
}
|
||||
|
||||
if payload.ProfileID < 1 {
|
||||
return errors.New("invalid profile id provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id fdoConfigureDevice
|
||||
// @summary configures an FDO device
|
||||
// @description configures an FDO device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param guid path int true "Guid"
|
||||
// @param body body deviceConfigurePayload true "Device Configuration"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/configure/{guid} [post]
|
||||
func (handler *Handler) fdoConfigureDevice(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
guid, err := request.RetrieveRouteVariableValue(r, "guid")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: request.RetrieveRouteVariableValue()")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: guid not found", err)
|
||||
}
|
||||
|
||||
var payload deviceConfigurePayload
|
||||
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("invalid request payload")
|
||||
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
profile, err := handler.DataStore.FDOProfile().Read(portainer.FDOProfileID(payload.ProfileID))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a FDO Profile with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find a FDO Profile with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(profile.FilePath, "")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: GetFileContent")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: GetFileContent", err)
|
||||
}
|
||||
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: newFDOClient()")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: newFDOClient()", err)
|
||||
}
|
||||
|
||||
// enable fdo_sys
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"0"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"active"},
|
||||
"bytes": []string{"F5"}, // this is "true" in CBOR
|
||||
}, []byte("")); err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw()", err)
|
||||
}
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_edgeid.txt"},
|
||||
}, []byte(payload.EdgeID)); err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: PutDeviceSVIRaw(edgeid)")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw(edgeid)", err)
|
||||
}
|
||||
|
||||
// write down the edgekey
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_edgekey.txt"},
|
||||
}, []byte(payload.EdgeKey)); err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: PutDeviceSVIRaw(edgekey)")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw(edgekey)", err)
|
||||
}
|
||||
|
||||
// write down the device name
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_name.txt"},
|
||||
}, []byte(payload.Name)); err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: PutDeviceSVIRaw(name)")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw(name)", err)
|
||||
}
|
||||
|
||||
// write down the device GUID - used as the EDGE_DEVICE_GUID too
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{"DEVICE_GUID.txt"},
|
||||
}, []byte(guid)); err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw()", err)
|
||||
}
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"1"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"filedesc"},
|
||||
"filename": []string{deploymentScriptName},
|
||||
}, fileContent); err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw()", err)
|
||||
}
|
||||
|
||||
b, err := cbor.Marshal([]string{"/bin/sh", deploymentScriptName})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to marshal string to CBOR")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw() failed to encode", err)
|
||||
}
|
||||
|
||||
cborBytes := strings.ToUpper(hex.EncodeToString(b))
|
||||
log.Debug().Str("cbor", cborBytes).Str("string", deploymentScriptName).Msg("converted to CBOR")
|
||||
|
||||
if err = fdoClient.PutDeviceSVIRaw(url.Values{
|
||||
"guid": []string{guid},
|
||||
"priority": []string{"2"},
|
||||
"module": []string{"fdo_sys"},
|
||||
"var": []string{"exec"},
|
||||
"bytes": []string{cborBytes},
|
||||
}, []byte("")); err != nil {
|
||||
log.Error().Err(err).Msg("fdoConfigureDevice: PutDeviceSVIRaw()")
|
||||
|
||||
return httperror.InternalServerError("fdoConfigureDevice: PutDeviceSVIRaw()", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/hostmanagement/fdo"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type fdoConfigurePayload portainer.FDOConfiguration
|
||||
|
||||
func validateURL(u string) error {
|
||||
p, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.Scheme != "http" && p.Scheme != "https" {
|
||||
return errors.New("invalid scheme provided, must be 'http' or 'https'")
|
||||
}
|
||||
|
||||
if p.Host == "" {
|
||||
return errors.New("invalid host provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (payload *fdoConfigurePayload) Validate(r *http.Request) error {
|
||||
if payload.Enabled {
|
||||
if err := validateURL(payload.OwnerURL); err != nil {
|
||||
return fmt.Errorf("owner server URL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) saveSettings(config portainer.FDOConfiguration) error {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settings.FDOConfiguration = config
|
||||
|
||||
return handler.DataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
|
||||
func (handler *Handler) newFDOClient() (fdo.FDOOwnerClient, error) {
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return fdo.FDOOwnerClient{}, err
|
||||
}
|
||||
|
||||
return fdo.FDOOwnerClient{
|
||||
OwnerURL: settings.FDOConfiguration.OwnerURL,
|
||||
Username: settings.FDOConfiguration.OwnerUsername,
|
||||
Password: settings.FDOConfiguration.OwnerPassword,
|
||||
Timeout: 5 * time.Second,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// @id fdoConfigure
|
||||
// @summary Enable Portainer's FDO capabilities
|
||||
// @description Enable Portainer's FDO capabilities
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @accept json
|
||||
// @produce json
|
||||
// @param body body fdoConfigurePayload true "FDO Settings"
|
||||
// @success 204 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo [post]
|
||||
func (handler *Handler) fdoConfigure(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload fdoConfigurePayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("invalid request payload")
|
||||
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
settings := portainer.FDOConfiguration(payload)
|
||||
if err = handler.saveSettings(settings); err != nil {
|
||||
return httperror.BadRequest("Error saving FDO settings", err)
|
||||
}
|
||||
|
||||
profiles, err := handler.DataStore.FDOProfile().ReadAll()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Error saving FDO settings", err)
|
||||
}
|
||||
|
||||
if len(profiles) == 0 {
|
||||
err = handler.addDefaultProfile()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
func (handler *Handler) addDefaultProfile() error {
|
||||
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
|
||||
profile := &portainer.FDOProfile{
|
||||
ID: portainer.FDOProfileID(profileID),
|
||||
Name: "Docker Standalone + Edge",
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(defaultProfileFileContent))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
profile.FilePath = filePath
|
||||
profile.DateCreated = time.Now().Unix()
|
||||
|
||||
return handler.DataStore.FDOProfile().Create(profile)
|
||||
}
|
||||
|
||||
const defaultProfileFileContent = `
|
||||
#!/bin/bash -ex
|
||||
|
||||
env > env.log
|
||||
|
||||
export AGENT_IMAGE=portainer/agent:2.11.0
|
||||
export GUID=$(cat DEVICE_GUID.txt)
|
||||
export DEVICE_NAME=$(cat DEVICE_name.txt)
|
||||
export EDGE_ID=$(cat DEVICE_edgeid.txt)
|
||||
export EDGE_KEY=$(cat DEVICE_edgekey.txt)
|
||||
export AGENTVOLUME=$(pwd)/data/portainer_agent_data/
|
||||
|
||||
mkdir -p ${AGENTVOLUME}
|
||||
|
||||
docker pull ${AGENT_IMAGE}
|
||||
|
||||
docker run -d \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v /var/lib/docker/volumes:/var/lib/docker/volumes \
|
||||
-v /:/host \
|
||||
-v ${AGENTVOLUME}:/data \
|
||||
--restart always \
|
||||
-e EDGE=1 \
|
||||
-e EDGE_ID=${EDGE_ID} \
|
||||
-e EDGE_KEY=${EDGE_KEY} \
|
||||
-e CAP_HOST_MANAGEMENT=1 \
|
||||
-e EDGE_INSECURE_POLL=1 \
|
||||
--name portainer_edge_agent \
|
||||
${AGENT_IMAGE}
|
||||
`
|
|
@ -1,40 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/dataservices"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore dataservices.DataStore
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
func NewHandler(bouncer security.BouncerService, dataStore dataservices.DataStore, fileService portainer.FileService) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
DataStore: dataStore,
|
||||
FileService: fileService,
|
||||
}
|
||||
|
||||
h.Handle("/fdo/configure", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigure))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/list", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoListAll))).Methods(http.MethodGet)
|
||||
h.Handle("/fdo/register", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoRegisterDevice))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/configure/{guid}", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoConfigureDevice))).Methods(http.MethodPost)
|
||||
|
||||
h.Handle("/fdo/profiles", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoProfileList))).Methods(http.MethodGet)
|
||||
h.Handle("/fdo/profiles", bouncer.AdminAccess(httperror.LoggerHandler(h.createProfile))).Methods(http.MethodPost)
|
||||
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.fdoProfileInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.updateProfile))).Methods(http.MethodPut)
|
||||
h.Handle("/fdo/profiles/{id}", bouncer.AdminAccess(httperror.LoggerHandler(h.deleteProfile))).Methods(http.MethodDelete)
|
||||
h.Handle("/fdo/profiles/{id}/duplicate", bouncer.AdminAccess(httperror.LoggerHandler(h.duplicateProfile))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// @id fdoListAll
|
||||
// @summary List all known FDO vouchers
|
||||
// @description List all known FDO vouchers
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/list [get]
|
||||
func (handler *Handler) fdoListAll(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("fdoListAll: newFDOClient()")
|
||||
|
||||
return httperror.InternalServerError("fdoRegisterDevice: newFDOClient()", err)
|
||||
}
|
||||
|
||||
// Get all vouchers
|
||||
guids, err := fdoClient.GetVouchers()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("fdoListAll: GetVouchers()")
|
||||
|
||||
return httperror.InternalServerError("fdoListAll: GetVouchers()", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, guids)
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type createProfileFromFileContentPayload struct {
|
||||
Name string
|
||||
ProfileFileContent string
|
||||
}
|
||||
|
||||
func (payload *createProfileFromFileContentPayload) Validate(r *http.Request) error {
|
||||
if payload.Name == "" {
|
||||
return errors.New("profile name must be provided")
|
||||
}
|
||||
|
||||
if payload.ProfileFileContent == "" {
|
||||
return errors.New("profile file content must be provided")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// @id createProfile
|
||||
// @summary creates a new FDO Profile
|
||||
// @description creates a new FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 409 "Profile name already exists"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles [post]
|
||||
func (handler *Handler) createProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
method, err := request.RetrieveQueryParameter(r, "method", false)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid query parameter: method", err)
|
||||
}
|
||||
|
||||
if method == "editor" {
|
||||
return handler.createFDOProfileFromFileContent(w, r)
|
||||
}
|
||||
|
||||
return httperror.BadRequest("Invalid method. Value must be one of: editor", errors.New("invalid method"))
|
||||
}
|
||||
|
||||
func (handler *Handler) createFDOProfileFromFileContent(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload createProfileFromFileContentPayload
|
||||
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
isUnique, err := handler.checkUniqueProfileName(payload.Name, -1)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
if !isUnique {
|
||||
return httperror.Conflict(fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), errors.New("a profile already exists with this name"))
|
||||
}
|
||||
|
||||
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
|
||||
profile := &portainer.FDOProfile{
|
||||
ID: portainer.FDOProfileID(profileID),
|
||||
Name: payload.Name,
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(payload.ProfileFileContent))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist profile file on disk", err)
|
||||
}
|
||||
|
||||
profile.FilePath = filePath
|
||||
profile.DateCreated = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.FDOProfile().Create(profile)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist the profile inside the database", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, profile)
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id deleteProfile
|
||||
// @summary deletes a FDO Profile
|
||||
// @description deletes a FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @param id path int true "FDO Profile identifier"
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id} [delete]
|
||||
func (handler *Handler) deleteProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Bad request", errors.New("missing 'id' query parameter"))
|
||||
}
|
||||
|
||||
err = handler.DataStore.FDOProfile().Delete(portainer.FDOProfileID(id))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to delete Profile", err)
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id duplicate
|
||||
// @summary duplicated an existing FDO Profile
|
||||
// @description duplicated an existing FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "FDO Profile identifier"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id}/duplicate [post]
|
||||
func (handler *Handler) duplicateProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Bad request", errors.New("missing 'id' query parameter"))
|
||||
}
|
||||
|
||||
originalProfile, err := handler.DataStore.FDOProfile().Read(portainer.FDOProfileID(id))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a FDO Profile with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find a FDO Profile with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(originalProfile.FilePath, "")
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Profile file content", err)
|
||||
}
|
||||
|
||||
profileID := handler.DataStore.FDOProfile().GetNextIdentifier()
|
||||
|
||||
newProfile := &portainer.FDOProfile{
|
||||
ID: portainer.FDOProfileID(profileID),
|
||||
Name: fmt.Sprintf("%s - copy", originalProfile.Name),
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(newProfile.ID)), fileContent)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist profile file on disk", err)
|
||||
}
|
||||
newProfile.FilePath = filePath
|
||||
newProfile.DateCreated = time.Now().Unix()
|
||||
|
||||
err = handler.DataStore.FDOProfile().Create(newProfile)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to persist the profile inside the database", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, newProfile)
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
type fdoProfileResponse struct {
|
||||
Name string `json:"name"`
|
||||
FileContent string `json:"fileContent"`
|
||||
}
|
||||
|
||||
// @id fdoProfileInspect
|
||||
// @summary retrieves a given FDO profile information and content
|
||||
// @description retrieves a given FDO profile information and content
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "FDO Profile identifier"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id} [get]
|
||||
func (handler *Handler) fdoProfileInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Bad request", errors.New("missing 'id' query parameter"))
|
||||
}
|
||||
|
||||
profile, err := handler.DataStore.FDOProfile().Read(portainer.FDOProfileID(id))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Profile", err)
|
||||
}
|
||||
|
||||
fileContent, err := handler.FileService.GetFileContent(profile.FilePath, "")
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to retrieve Profile file content", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, fdoProfileResponse{
|
||||
Name: profile.Name,
|
||||
FileContent: string(fileContent),
|
||||
})
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id fdoProfileList
|
||||
// @summary retrieves all FDO profiles
|
||||
// @description retrieves all FDO profiles
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @failure 500 "Bad gateway"
|
||||
// @router /fdo/profiles [get]
|
||||
func (handler *Handler) fdoProfileList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
|
||||
profiles, err := handler.DataStore.FDOProfile().ReadAll()
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
|
||||
return response.JSON(w, profiles)
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
)
|
||||
|
||||
// @id updateProfile
|
||||
// @summary updates an existing FDO Profile
|
||||
// @description updates an existing FDO Profile
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @param id path int true "FDO Profile identifier"
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 409 "Profile name already exists"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/profiles/{id} [put]
|
||||
func (handler *Handler) updateProfile(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
id, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Bad request", errors.New("missing 'id' query parameter"))
|
||||
}
|
||||
|
||||
var payload createProfileFromFileContentPayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return httperror.BadRequest("Invalid request payload", err)
|
||||
}
|
||||
|
||||
profile, err := handler.DataStore.FDOProfile().Read(portainer.FDOProfileID(id))
|
||||
if handler.DataStore.IsErrObjectNotFound(err) {
|
||||
return httperror.NotFound("Unable to find a FDO Profile with the specified identifier inside the database", err)
|
||||
} else if err != nil {
|
||||
return httperror.InternalServerError("Unable to find a FDO Profile with the specified identifier inside the database", err)
|
||||
}
|
||||
|
||||
isUnique, err := handler.checkUniqueProfileName(payload.Name, id)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError(err.Error(), err)
|
||||
}
|
||||
if !isUnique {
|
||||
return httperror.Conflict(fmt.Sprintf("A profile with the name '%s' already exists", payload.Name), errors.New("a profile already exists with this name"))
|
||||
}
|
||||
|
||||
filePath, err := handler.FileService.StoreFDOProfileFileFromBytes(strconv.Itoa(int(profile.ID)), []byte(payload.ProfileFileContent))
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update profile", err)
|
||||
}
|
||||
profile.FilePath = filePath
|
||||
profile.Name = payload.Name
|
||||
|
||||
err = handler.DataStore.FDOProfile().Update(profile.ID, profile)
|
||||
if err != nil {
|
||||
return httperror.InternalServerError("Unable to update profile", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, profile)
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package fdo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||
"github.com/portainer/portainer/pkg/libhttp/response"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type registerDeviceResponse struct {
|
||||
Guid string `json:"guid" example:"c6ea3343-229a-4c07-9096-beef7134e1d3"`
|
||||
}
|
||||
|
||||
// @id fdoRegisterDevice
|
||||
// @summary register an FDO device
|
||||
// @description register an FDO device
|
||||
// @description **Access policy**: administrator
|
||||
// @tags intel
|
||||
// @security jwt
|
||||
// @produce json
|
||||
// @success 200 "Success"
|
||||
// @failure 400 "Invalid request"
|
||||
// @failure 403 "Permission denied to access settings"
|
||||
// @failure 500 "Server error"
|
||||
// @router /fdo/register [post]
|
||||
func (handler *Handler) fdoRegisterDevice(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
// Post a voucher
|
||||
ov, filename, err := request.RetrieveMultiPartFormFile(r, "voucher")
|
||||
if err != nil {
|
||||
log.Info().Str("filename", filename).Err(err).Msg("fdoRegisterDevice: readVoucher()")
|
||||
|
||||
return httperror.InternalServerError("fdoRegisterDevice: read Voucher()", err)
|
||||
}
|
||||
|
||||
fdoClient, err := handler.newFDOClient()
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("fdoRegisterDevice: newFDOClient()")
|
||||
|
||||
return httperror.InternalServerError("fdoRegisterDevice: newFDOClient()", err)
|
||||
}
|
||||
|
||||
guid, err := fdoClient.PostVoucher(ov)
|
||||
if err != nil {
|
||||
log.Info().Err(err).Msg("fdoRegisterDevice: PostVoucher()")
|
||||
|
||||
return httperror.InternalServerError("fdoRegisterDevice: PostVoucher()", err)
|
||||
}
|
||||
|
||||
return response.JSON(w, registerDeviceResponse{guid})
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
package fdo
|
||||
|
||||
func (handler *Handler) checkUniqueProfileName(name string, id int) (bool, error) {
|
||||
profiles, err := handler.DataStore.FDOProfile().ReadAll()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, profile := range profiles {
|
||||
if profile.Name == name && (id == -1 || id != int(profile.ID)) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
|
@ -36,8 +36,6 @@ type publicSettingsResponse struct {
|
|||
// Whether team sync is enabled
|
||||
TeamSync bool `json:"TeamSync" example:"true"`
|
||||
|
||||
// Whether FDO is enabled
|
||||
IsFDOEnabled bool
|
||||
// Whether AMT is enabled
|
||||
IsAMTEnabled bool
|
||||
|
||||
|
@ -86,7 +84,6 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
|||
EnableTelemetry: appSettings.EnableTelemetry,
|
||||
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
||||
Features: featureflags.FeatureFlags(),
|
||||
IsFDOEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.FDOConfiguration.Enabled,
|
||||
IsAMTEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.OpenAMTConfiguration.Enabled,
|
||||
}
|
||||
|
||||
|
|
|
@ -32,7 +32,6 @@ import (
|
|||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/gitops"
|
||||
"github.com/portainer/portainer/api/http/handler/helm"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
|
||||
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
|
||||
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||
|
@ -239,8 +238,6 @@ func (server *Server) Start() error {
|
|||
openAMTHandler.DataStore = server.DataStore
|
||||
openAMTHandler.DockerClientFactory = server.DockerClientFactory
|
||||
|
||||
fdoHandler := fdo.NewHandler(requestBouncer, server.DataStore, server.FileService)
|
||||
|
||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||
stackHandler.DataStore = server.DataStore
|
||||
stackHandler.DockerClientFactory = server.DockerClientFactory
|
||||
|
@ -316,7 +313,6 @@ func (server *Server) Start() error {
|
|||
KubernetesHandler: kubernetesHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
OpenAMTHandler: openAMTHandler,
|
||||
FDOHandler: fdoHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
|
|
|
@ -17,7 +17,6 @@ type testDatastore struct {
|
|||
endpoint dataservices.EndpointService
|
||||
endpointGroup dataservices.EndpointGroupService
|
||||
endpointRelation dataservices.EndpointRelationService
|
||||
fdoProfile dataservices.FDOProfileService
|
||||
helmUserRepository dataservices.HelmUserRepositoryService
|
||||
registry dataservices.RegistryService
|
||||
resourceControl dataservices.ResourceControlService
|
||||
|
@ -55,10 +54,6 @@ func (d *testDatastore) EdgeStack() dataservices.EdgeStackService { re
|
|||
func (d *testDatastore) Endpoint() dataservices.EndpointService { return d.endpoint }
|
||||
func (d *testDatastore) EndpointGroup() dataservices.EndpointGroupService { return d.endpointGroup }
|
||||
|
||||
func (d *testDatastore) FDOProfile() dataservices.FDOProfileService {
|
||||
return d.fdoProfile
|
||||
}
|
||||
|
||||
func (d *testDatastore) EndpointRelation() dataservices.EndpointRelationService {
|
||||
return d.endpointRelation
|
||||
}
|
||||
|
|
|
@ -96,24 +96,6 @@ type (
|
|||
// PowerState represents an AMT managed device power state
|
||||
PowerState int
|
||||
|
||||
FDOConfiguration struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
OwnerURL string `json:"ownerURL"`
|
||||
OwnerUsername string `json:"ownerUsername"`
|
||||
OwnerPassword string `json:"ownerPassword"`
|
||||
}
|
||||
|
||||
// FDOProfileID represents a fdo profile id
|
||||
FDOProfileID int
|
||||
|
||||
FDOProfile struct {
|
||||
ID FDOProfileID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
FilePath string `json:"filePath"`
|
||||
NumberDevices int `json:"numberDevices"`
|
||||
DateCreated int64 `json:"dateCreated"`
|
||||
}
|
||||
|
||||
// CLIFlags represents the available flags on the CLI
|
||||
CLIFlags struct {
|
||||
Addr *string
|
||||
|
@ -963,7 +945,6 @@ type (
|
|||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||
OpenAMTConfiguration OpenAMTConfiguration `json:"openAMTConfiguration"`
|
||||
FDOConfiguration FDOConfiguration `json:"fdoConfiguration"`
|
||||
FeatureFlagSettings map[featureflags.Feature]bool `json:"FeatureFlagSettings"`
|
||||
// The interval in which environment(endpoint) snapshots are created
|
||||
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
|
||||
|
@ -1456,7 +1437,6 @@ type (
|
|||
StoreSSLCertPair(cert, key []byte) (string, string, error)
|
||||
CopySSLCertPair(certPath, keyPath string) (string, string, error)
|
||||
CopySSLCACert(caCertPath string) (string, error)
|
||||
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
|
||||
StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error)
|
||||
GetDefaultChiselPrivateKeyPath() string
|
||||
StoreChiselPrivateKey(privateKey []byte) error
|
||||
|
@ -1646,13 +1626,7 @@ const (
|
|||
)
|
||||
|
||||
// List of supported features
|
||||
const (
|
||||
FeatureFdo = "fdo"
|
||||
)
|
||||
|
||||
var SupportedFeatureFlags = []featureflags.Feature{
|
||||
FeatureFdo,
|
||||
}
|
||||
var SupportedFeatureFlags = []featureflags.Feature{}
|
||||
|
||||
const (
|
||||
_ AuthenticationMethod = iota
|
||||
|
|
|
@ -200,17 +200,6 @@ angular
|
|||
},
|
||||
};
|
||||
|
||||
var deviceImport = {
|
||||
name: 'portainer.endpoints.importDevice',
|
||||
url: '/device',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: './views/devices/import/importDevice.html',
|
||||
controller: 'ImportDeviceController',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const edgeAutoCreateScript = {
|
||||
name: 'portainer.endpoints.edgeAutoCreateScript',
|
||||
url: '/aeec',
|
||||
|
@ -224,26 +213,6 @@ angular
|
|||
},
|
||||
};
|
||||
|
||||
var addFDOProfile = {
|
||||
name: 'portainer.endpoints.profile',
|
||||
url: '/profile',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'addProfileView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var editFDOProfile = {
|
||||
name: 'portainer.endpoints.profile.edit',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
component: 'editProfileView',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var endpointAccess = {
|
||||
name: 'portainer.endpoints.endpoint.access',
|
||||
url: '/access',
|
||||
|
@ -484,9 +453,6 @@ angular
|
|||
$stateRegistryProvider.register(endpointAccess);
|
||||
$stateRegistryProvider.register(endpointKVM);
|
||||
$stateRegistryProvider.register(edgeAutoCreateScript);
|
||||
$stateRegistryProvider.register(deviceImport);
|
||||
$stateRegistryProvider.register(addFDOProfile);
|
||||
$stateRegistryProvider.register(editFDOProfile);
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(group);
|
||||
$stateRegistryProvider.register(groupAccess);
|
||||
|
|
|
@ -1,99 +0,0 @@
|
|||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { FDOConfiguration, DeviceConfiguration, Profile } from './model';
|
||||
|
||||
const BASE_URL = '/fdo';
|
||||
|
||||
export async function configureFDO(formValues: FDOConfiguration) {
|
||||
try {
|
||||
await axios.post(`${BASE_URL}/configure`, formValues);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to configure FDO');
|
||||
}
|
||||
}
|
||||
|
||||
export async function configureDevice(
|
||||
deviceId: string,
|
||||
deviceConfig: DeviceConfiguration
|
||||
) {
|
||||
try {
|
||||
await axios.post(`${BASE_URL}/configure/${deviceId}`, deviceConfig);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to configure device');
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProfile(
|
||||
name: string,
|
||||
method: string,
|
||||
profileFileContent: string
|
||||
) {
|
||||
const payload = {
|
||||
name,
|
||||
profileFileContent,
|
||||
};
|
||||
try {
|
||||
await axios.post(`${BASE_URL}/profiles`, payload, {
|
||||
params: { method },
|
||||
});
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to create profile');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProfiles() {
|
||||
try {
|
||||
const { data: profiles } = await axios.get<Profile[]>(
|
||||
`${BASE_URL}/profiles`
|
||||
);
|
||||
return profiles;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve the profiles');
|
||||
}
|
||||
}
|
||||
|
||||
export async function getProfile(profileId: number) {
|
||||
try {
|
||||
const { data: profile } = await axios.get<Profile>(
|
||||
`${BASE_URL}/profiles/${profileId}`
|
||||
);
|
||||
return profile;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to retrieve profile');
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteProfile(profileId: number) {
|
||||
try {
|
||||
await axios.delete(`${BASE_URL}/profiles/${profileId}`);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to delete profile');
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateProfile(
|
||||
id: number,
|
||||
name: string,
|
||||
profileFileContent: string
|
||||
) {
|
||||
const payload = {
|
||||
name,
|
||||
profileFileContent,
|
||||
};
|
||||
try {
|
||||
await axios.put(`${BASE_URL}/profiles/${id}`, payload);
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to update profile');
|
||||
}
|
||||
}
|
||||
|
||||
export async function duplicateProfile(id: number) {
|
||||
try {
|
||||
const { data: profile } = await axios.post<Profile>(
|
||||
`${BASE_URL}/profiles/${id}/duplicate`
|
||||
);
|
||||
return profile;
|
||||
} catch (e) {
|
||||
throw parseAxiosError(e as Error, 'Unable to duplicate profile');
|
||||
}
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
export interface FDOConfiguration {
|
||||
enabled: boolean;
|
||||
ownerURL: string;
|
||||
ownerUsername: string;
|
||||
ownerPassword: string;
|
||||
}
|
||||
|
||||
export interface DeviceConfiguration {
|
||||
edgeID: string;
|
||||
edgeKey: string;
|
||||
name: string;
|
||||
profile: string;
|
||||
}
|
||||
|
||||
export type Profile = {
|
||||
id: number;
|
||||
name: string;
|
||||
fileContent: string;
|
||||
dateCreated: string;
|
||||
};
|
|
@ -6,7 +6,6 @@ export function SettingsViewModel(data) {
|
|||
this.LDAPSettings = data.LDAPSettings;
|
||||
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
|
||||
this.openAMTConfiguration = data.openAMTConfiguration;
|
||||
this.fdoConfiguration = data.fdoConfiguration;
|
||||
this.SnapshotInterval = data.SnapshotInterval;
|
||||
this.TemplatesURL = data.TemplatesURL;
|
||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||
|
@ -37,7 +36,6 @@ export function PublicSettingsViewModel(settings) {
|
|||
this.Edge = new EdgeSettingsViewModel(settings.Edge);
|
||||
this.DefaultRegistry = settings.DefaultRegistry;
|
||||
this.IsAMTEnabled = settings.IsAMTEnabled;
|
||||
this.IsFDOEnabled = settings.IsFDOEnabled;
|
||||
}
|
||||
|
||||
export function InternalAuthSettingsViewModel(data) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { SettingsFDO } from '@/react/portainer/settings/EdgeComputeView/SettingsFDO';
|
||||
import { SettingsOpenAMT } from '@/react/portainer/settings/EdgeComputeView/SettingsOpenAMT';
|
||||
import { InternalAuth } from '@/react/portainer/settings/AuthenticationView/InternalAuth';
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
|
@ -17,10 +16,6 @@ import { AuthStyleField } from '@/react/portainer/settings/AuthenticationView/OA
|
|||
|
||||
export const settingsModule = angular
|
||||
.module('portainer.app.react.components.settings', [])
|
||||
.component(
|
||||
'settingsFdo',
|
||||
r2a(withUIRouter(withReactQuery(SettingsFDO)), ['onSubmit', 'settings'])
|
||||
)
|
||||
.component('settingsOpenAmt', r2a(SettingsOpenAMT, ['onSubmit', 'settings']))
|
||||
.component(
|
||||
'internalAuth',
|
||||
|
|
|
@ -195,15 +195,5 @@ function FileUploadFactory($q, Upload) {
|
|||
return $q.all(queue);
|
||||
};
|
||||
|
||||
service.uploadOwnershipVoucher = function (voucherFile) {
|
||||
return Upload.upload({
|
||||
url: 'api/fdo/register',
|
||||
data: {
|
||||
voucher: voucherFile,
|
||||
},
|
||||
ignoreLoadingBar: true,
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
<page-header title="'FDO Device Configuration'" breadcrumbs="[{label:'Environments', link:'portainer.endpoints'}, 'Import FDO Device']" reload="true"> </page-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="wand-2" title-text="Import Device Set up"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="fdoForm">
|
||||
<!-- info -->
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
|
||||
You are setting up a Portainer Edge Agent that will initiate the communications with the Portainer instance and your FDO Devices.
|
||||
</p>
|
||||
</span>
|
||||
<!-- !info -->
|
||||
<!-- import voucher -->
|
||||
<div class="col-sm-12 form-section-title"> Import Voucher </div>
|
||||
<div>
|
||||
<div class="form-group" ng-show="!state.vouchersUploaded">
|
||||
<span class="small col-sm-12">
|
||||
<p class="text-muted" style="margin-top: 10px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
|
||||
Import one or more Manufacturer's Ownership Vouchers to initiate device attestation
|
||||
</p>
|
||||
</span>
|
||||
<div class="col-sm-8">
|
||||
<button
|
||||
style="margin-left: 0px !important"
|
||||
class="btn btn-sm btn-primary"
|
||||
ngf-select="onVoucherFilesChange()"
|
||||
ng-model="formValues.VoucherFiles"
|
||||
name="VoucherFiles"
|
||||
ng-disabled="state.vouchersUploading"
|
||||
button-spinner="state.vouchersUploading"
|
||||
multiple
|
||||
>
|
||||
<span ng-hide="state.vouchersUploading"
|
||||
>Upload
|
||||
<pr-icon icon="'upload'" class-name="'ml-1'"></pr-icon>
|
||||
</span>
|
||||
<span ng-show="state.vouchersUploading">Uploading Voucher...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="state.vouchersUploading">
|
||||
<div class="col-sm-12 small text-success">
|
||||
<p>Connecting to the Owner service...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="state.vouchersUploaded">
|
||||
<div class="col-sm-12">
|
||||
<p>Ownership Voucher Uploaded <pr-icon icon="'check'" mode="'success'"></pr-icon></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !import voucher -->
|
||||
<!-- device details -->
|
||||
<div class="col-sm-12 form-section-title"> Device details </div>
|
||||
<div>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
|
||||
Device name will serve as your reference name in Portainer
|
||||
</p>
|
||||
</span>
|
||||
<!-- device name input -->
|
||||
<div class="form-group">
|
||||
<label for="device_name" class="col-sm-3 col-lg-2 control-label text-left">Device Name</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="deviceImport-deviceNameInput"
|
||||
class="form-control"
|
||||
name="device_name"
|
||||
placeholder="e.g. FDO-Test01"
|
||||
ng-model="formValues.DeviceName"
|
||||
ng-required="state.vouchersUploaded"
|
||||
ng-disabled="!state.vouchersUploaded"
|
||||
auto-focus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="fdoForm.device_name.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="fdoForm.device_name.$error">
|
||||
<p ng-message="required">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
This field is required.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !device name input -->
|
||||
<!-- suffix input -->
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
|
||||
Suffix starting number will be appended to the end of the Device name, if initiating multiple devices this will be incrementally increased
|
||||
</p>
|
||||
</span>
|
||||
<div class="form-group">
|
||||
<label for="suffix" class="col-sm-3 col-lg-2 control-label text-left"> Suffix starting number </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="deviceImport-suffixInput"
|
||||
class="form-control"
|
||||
name="suffix"
|
||||
ng-model="formValues.Suffix"
|
||||
ng-required="state.vouchersUploaded"
|
||||
ng-disabled="!state.vouchersUploaded"
|
||||
ng-pattern="/^[0-9]+$/"
|
||||
placeholder="1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="fdoForm.suffix.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="fdoForm.suffix.$error">
|
||||
<p ng-message="required">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
This field is required.</p
|
||||
>
|
||||
<p ng-message="pattern">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
This field needs to be a positive integer number.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !suffix input -->
|
||||
<!-- portainer-instance-input -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_url" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Portainer server URL
|
||||
<portainer-tooltip message="'URL of the Portainer instance that the agent will use to initiate the communications.'"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input
|
||||
type="text"
|
||||
data-cy="deviceImport-portainerServerUrlInput"
|
||||
class="form-control"
|
||||
name="endpoint_url"
|
||||
ng-model="formValues.PortainerURL"
|
||||
ng-required="state.vouchersUploaded"
|
||||
ng-disabled="!state.vouchersUploaded"
|
||||
placeholder="e.g. https://10.0.0.10:9443"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="fdoForm.endpoint_url.$invalid">
|
||||
<div class="col-sm-12 small text-warning">
|
||||
<div ng-messages="fdoForm.endpoint_url.$error">
|
||||
<p ng-message="required">
|
||||
<pr-icon icon="'alert-triangle'" mode="'warning'"></pr-icon>
|
||||
This field is required.</p
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !portainer-instance-input -->
|
||||
</div>
|
||||
<!-- device profile input -->
|
||||
<div class="form-group">
|
||||
<label for="device_profile" class="col-sm-3 col-lg-2 control-label text-left">Device Profile</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select
|
||||
id="device_profile"
|
||||
data-cy="deviceImport-deviceProfileSelect"
|
||||
ng-model="formValues.DeviceProfile"
|
||||
class="form-control"
|
||||
ng-required="state.vouchersUploaded"
|
||||
ng-disabled="!state.vouchersUploaded"
|
||||
>
|
||||
<option selected disabled hidden value="">Select a profile for your device</option>
|
||||
<option ng-repeat="profile in profiles | orderBy: 'name'" ng-value="profile.id">{{ profile.name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !device profile input -->
|
||||
<!-- !device details -->
|
||||
<!-- tags -->
|
||||
<div class="col-sm-12 form-section-title"> Set up Tags </div>
|
||||
<div>
|
||||
<span class="small">
|
||||
<p class="text-muted" style="margin-top: 10px">
|
||||
<pr-icon icon="'info'" mode="'primary'" class-name="'mr-0.5'"></pr-icon>
|
||||
This is just an option if your device is under a certain group
|
||||
</p>
|
||||
</span>
|
||||
<!-- group -->
|
||||
<div class="form-group">
|
||||
<label for="device_group" class="col-sm-3 col-lg-2 control-label text-left"> Group </label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select
|
||||
class="form-control"
|
||||
data-cy="deviceImport-deviceGroupSelect"
|
||||
ng-options="group.Id as group.Name for group in groups"
|
||||
ng-model="formValues.GroupId"
|
||||
id="device_group"
|
||||
ng-required="state.vouchersUploaded"
|
||||
ng-disabled="!state.vouchersUploaded"
|
||||
data-cy="deviceImport-deviceGroup"
|
||||
></select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !group -->
|
||||
|
||||
<tag-selector ng-if="formValues" value="formValues.TagIds" allow-create="state.allowCreateTag" on-change="(onChangeTags)"> </tag-selector>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="createEndpointAndConfigureDevice()"
|
||||
ng-disabled="state.actionInProgress || !state.vouchersUploaded || !fdoForm.$valid"
|
||||
button-spinner="state.actionInProgress"
|
||||
data-cy="deviceImport-saveDeviceButton"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Save Configuration</span>
|
||||
<span ng-show="state.actionInProgress">Saving...</span>
|
||||
</button>
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.endpoints">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -1,138 +0,0 @@
|
|||
import uuidv4 from 'uuid/v4';
|
||||
|
||||
import { PortainerEndpointCreationTypes } from 'Portainer/models/endpoint/models';
|
||||
import { configureDevice, getProfiles } from 'Portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
angular
|
||||
.module('portainer.app')
|
||||
.controller(
|
||||
'ImportDeviceController',
|
||||
function ImportDeviceController($async, $q, $scope, $state, EndpointService, GroupService, TagService, Notifications, Authentication, FileUploadService) {
|
||||
$scope.state = {
|
||||
actionInProgress: false,
|
||||
vouchersUploading: false,
|
||||
vouchersUploaded: false,
|
||||
deviceIDs: [],
|
||||
allowCreateTag: Authentication.isAdmin(),
|
||||
};
|
||||
|
||||
$scope.formValues = {
|
||||
DeviceName: '',
|
||||
DeviceProfile: '',
|
||||
GroupId: 1,
|
||||
TagIds: [],
|
||||
VoucherFiles: [],
|
||||
PortainerURL: '',
|
||||
Suffix: 1,
|
||||
};
|
||||
|
||||
$scope.profiles = [];
|
||||
|
||||
$scope.onChangeTags = function onChangeTags(value) {
|
||||
return $scope.$evalAsync(() => {
|
||||
$scope.formValues.TagIds = value;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onVoucherFilesChange = function () {
|
||||
if ($scope.formValues.VoucherFiles.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.state.vouchersUploading = true;
|
||||
|
||||
let uploads = $scope.formValues.VoucherFiles.map((f) => FileUploadService.uploadOwnershipVoucher(f));
|
||||
|
||||
$q.all(uploads)
|
||||
.then(function success(responses) {
|
||||
$scope.state.vouchersUploading = false;
|
||||
$scope.state.vouchersUploaded = true;
|
||||
$scope.state.deviceIDs = responses.map((r) => r.data.guid);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.state.vouchersUploading = false;
|
||||
if ($scope.formValues.VoucherFiles.length === 1) {
|
||||
Notifications.error('Failure', err, 'Unable to upload the Ownership Voucher');
|
||||
} else {
|
||||
Notifications.error('Failure', null, 'Unable to upload the Ownership Vouchers, please check the logs');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createEndpointAndConfigureDevice = function () {
|
||||
return $async(async () => {
|
||||
$scope.state.actionInProgress = true;
|
||||
|
||||
let suffix = $scope.formValues.Suffix;
|
||||
|
||||
for (const deviceID of $scope.state.deviceIDs) {
|
||||
let deviceName = $scope.formValues.DeviceName + suffix;
|
||||
|
||||
try {
|
||||
var endpoint = await EndpointService.createRemoteEndpoint(
|
||||
deviceName,
|
||||
PortainerEndpointCreationTypes.EdgeAgentEnvironment,
|
||||
$scope.formValues.PortainerURL,
|
||||
'',
|
||||
$scope.formValues.GroupId,
|
||||
$scope.formValues.TagIds,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to create the environment');
|
||||
$scope.state.actionInProgress = false;
|
||||
return;
|
||||
}
|
||||
|
||||
suffix++;
|
||||
|
||||
const config = {
|
||||
edgeID: endpoint.EdgeID || uuidv4(),
|
||||
edgeKey: endpoint.EdgeKey,
|
||||
name: deviceName,
|
||||
profile: $scope.formValues.DeviceProfile,
|
||||
};
|
||||
|
||||
try {
|
||||
await configureDevice(deviceID, config);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to import device');
|
||||
return;
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
Notifications.success('Success', 'Device(s) successfully imported');
|
||||
$state.go('edge.devices');
|
||||
});
|
||||
};
|
||||
|
||||
async function initView() {
|
||||
try {
|
||||
$scope.profiles = await getProfiles();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to load profiles');
|
||||
return;
|
||||
}
|
||||
|
||||
$q.all({
|
||||
groups: GroupService.groups(),
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.groups = data.groups;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to load groups');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}
|
||||
);
|
|
@ -1,65 +0,0 @@
|
|||
<page-header title="'Create profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']" reload="true"> </page-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="createProfileForm">
|
||||
<!-- name-input -->
|
||||
<div class="col-sm-12 form-section-title">Device Profile Details </div>
|
||||
<div class="form-group">
|
||||
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="formValues.name"
|
||||
id="profile_name"
|
||||
name="profile_name"
|
||||
placeholder="e.g. myprofile"
|
||||
auto-focus
|
||||
data-cy="profile-name-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- build-method -->
|
||||
<div class="col-sm-12 form-section-title"> Profile configuration </div>
|
||||
<box-selector slim="true" options="buildMethods" value="state.method"></box-selector>
|
||||
|
||||
<!-- !build-method -->
|
||||
|
||||
<web-editor-form
|
||||
ng-if="state.method === 'editor'"
|
||||
identifier="profile-creation-editor"
|
||||
value="formValues.profileFileContent"
|
||||
on-change="(onChangeFileContent)"
|
||||
ng-required="true"
|
||||
>
|
||||
</web-editor-form>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.settings.edgeCompute">Cancel</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress
|
||||
|| !createProfileForm.$valid
|
||||
|| !formValues.profileFileContent
|
||||
|| !formValues.name"
|
||||
ng-click="createProfileAsync()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Save Profile</span>
|
||||
<span ng-show="state.actionInProgress">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -1,70 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import { editor } from '@@/BoxSelector/common-options/build-methods';
|
||||
|
||||
import { createProfile } from 'Portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
angular.module('portainer.app').controller('AddProfileController', AddProfileController);
|
||||
|
||||
/* @ngInject */
|
||||
export default function AddProfileController($scope, $async, $state, $window, Notifications) {
|
||||
$scope.buildMethods = [editor];
|
||||
|
||||
$scope.formValues = {
|
||||
name: '',
|
||||
profileFileContent: '',
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
method: 'editor',
|
||||
actionInProgress: false,
|
||||
isEditorDirty: false,
|
||||
};
|
||||
|
||||
$window.onbeforeunload = () => {
|
||||
if ($scope.state.method === 'editor' && $scope.formValues.profileFileContent && $scope.state.isEditorDirty) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$scope.state.isEditorDirty = false;
|
||||
});
|
||||
|
||||
$scope.onChangeFormValues = onChangeFormValues;
|
||||
|
||||
$scope.createProfileAsync = function () {
|
||||
return $async(async () => {
|
||||
const method = $scope.state.method;
|
||||
|
||||
const name = $scope.formValues.name;
|
||||
const fileContent = $scope.formValues.profileFileContent;
|
||||
|
||||
if (method !== 'editor' && fileContent === '') {
|
||||
$scope.state.formValidationError = 'Profile file content must not be empty';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
await createProfile(name, method, fileContent);
|
||||
Notifications.success('Success', 'Profile successfully created');
|
||||
$scope.state.isEditorDirty = false;
|
||||
$state.go('portainer.settings.edgeCompute');
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to create Profile');
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onChangeFileContent = function onChangeFileContent(value) {
|
||||
$scope.formValues.profileFileContent = value;
|
||||
$scope.state.isEditorDirty = true;
|
||||
};
|
||||
|
||||
function onChangeFormValues(newValues) {
|
||||
$scope.formValues = newValues;
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import controller from './addProfileController';
|
||||
|
||||
angular.module('portainer.app').component('addProfileView', {
|
||||
templateUrl: './addProfile.html',
|
||||
controller,
|
||||
});
|
|
@ -1,66 +0,0 @@
|
|||
<page-header title="'Edit profile'" breadcrumbs="[{label:'Settings', link:'portainer.settings'}, 'Edge Compute']" reload="true"> </page-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal" name="editProfileForm">
|
||||
<!-- name-input -->
|
||||
<div class="col-sm-12 form-section-title">Device Profile Details </div>
|
||||
<div class="form-group">
|
||||
<label for="stack_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-11">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
ng-model="formValues.name"
|
||||
id="profile_name"
|
||||
name="profile_name"
|
||||
placeholder="e.g. myprofile"
|
||||
auto-focus
|
||||
data-cy="profile-name-input"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- build-method -->
|
||||
<div class="col-sm-12 form-section-title"> Profile configuration </div>
|
||||
|
||||
<box-selector slim="true" options="buildMethods" value="state.method"></box-selector>
|
||||
|
||||
<!-- !build-method -->
|
||||
|
||||
<web-editor-form
|
||||
ng-if="state.method === 'editor'"
|
||||
identifier="profile-creation-editor"
|
||||
value="formValues.profileFileContent"
|
||||
on-change="(onChangeFileContent)"
|
||||
ng-required="true"
|
||||
>
|
||||
</web-editor-form>
|
||||
|
||||
<!-- actions -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<a type="button" class="btn btn-default btn-sm" ui-sref="portainer.settings.edgeCompute">Cancel</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-disabled="state.actionInProgress
|
||||
|| !editProfileForm.$valid
|
||||
|| !formValues.profileFileContent
|
||||
|| !formValues.name"
|
||||
ng-click="updateProfileAsync()"
|
||||
button-spinner="state.actionInProgress"
|
||||
>
|
||||
<span ng-hide="state.actionInProgress">Update Profile</span>
|
||||
<span ng-show="state.actionInProgress">Saving...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
|
@ -1,88 +0,0 @@
|
|||
import angular from 'angular';
|
||||
import { editor } from '@@/BoxSelector/common-options/build-methods';
|
||||
import { getProfile, updateProfile } from 'Portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
angular.module('portainer.app').controller('EditProfileController', EditProfileController);
|
||||
|
||||
/* @ngInject */
|
||||
export default function EditProfileController($scope, $async, $state, $window, Notifications) {
|
||||
$scope.buildMethods = [editor];
|
||||
|
||||
$scope.formValues = {
|
||||
name: '',
|
||||
profileFileContent: '',
|
||||
};
|
||||
|
||||
$scope.state = {
|
||||
profileID: $state.params.id,
|
||||
method: 'editor',
|
||||
actionInProgress: false,
|
||||
isEditorDirty: false,
|
||||
};
|
||||
|
||||
$window.onbeforeunload = () => {
|
||||
if ($scope.state.method === 'editor' && $scope.formValues.profileFileContent && $scope.state.isEditorDirty) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function () {
|
||||
$scope.state.isEditorDirty = false;
|
||||
});
|
||||
|
||||
$scope.onChangeFormValues = onChangeFormValues;
|
||||
|
||||
$scope.updateProfileAsync = function () {
|
||||
return $async(async () => {
|
||||
const method = $scope.state.method;
|
||||
|
||||
const name = $scope.formValues.name;
|
||||
const fileContent = $scope.formValues.profileFileContent;
|
||||
|
||||
if (method !== 'editor' && fileContent === '') {
|
||||
$scope.state.formValidationError = 'Profile file content must not be empty';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
|
||||
try {
|
||||
await updateProfile($scope.state.profileID, name, fileContent);
|
||||
Notifications.success('Success', 'Profile successfully updated');
|
||||
$scope.state.isEditorDirty = false;
|
||||
$state.go('portainer.settings.edgeCompute');
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to update Profile');
|
||||
} finally {
|
||||
$scope.state.actionInProgress = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.onChangeFileContent = function onChangeFileContent(value) {
|
||||
$scope.formValues.profileFileContent = value;
|
||||
$scope.state.isEditorDirty = true;
|
||||
};
|
||||
|
||||
function onChangeFormValues(newValues) {
|
||||
$scope.formValues = newValues;
|
||||
}
|
||||
|
||||
async function initView() {
|
||||
return $async(async () => {
|
||||
try {
|
||||
const profile = await getProfile($scope.state.profileID);
|
||||
|
||||
$scope.formValues = {
|
||||
name: profile.name,
|
||||
profileFileContent: profile.fileContent,
|
||||
};
|
||||
$scope.state.isEditorDirty = false;
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve profile details');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import controller from './editProfileController';
|
||||
|
||||
angular.module('portainer.app').component('editProfileView', {
|
||||
templateUrl: './editProfile.html',
|
||||
controller,
|
||||
});
|
|
@ -11,9 +11,3 @@
|
|||
<settings-open-amt on-submit="($ctrl.onSubmitOpenAMT)" settings="($ctrl.settings)"></settings-open-amt>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12" ng-if="$ctrl.settings">
|
||||
<settings-fdo on-submit="($ctrl.onSubmitFDO)" settings="($ctrl.settings)"></settings-fdo>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import _ from 'lodash-es';
|
||||
import angular from 'angular';
|
||||
|
||||
import { configureFDO } from '@/portainer/hostmanagement/fdo/fdo.service';
|
||||
import { configureAMT } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
||||
|
||||
angular.module('portainer.app').controller('SettingsEdgeComputeController', SettingsEdgeComputeController);
|
||||
|
@ -31,16 +30,6 @@ export default function SettingsEdgeComputeController($q, $async, $state, Notifi
|
|||
}
|
||||
};
|
||||
|
||||
this.onSubmitFDO = async function (formValues) {
|
||||
try {
|
||||
await configureFDO(formValues);
|
||||
Notifications.success('Success', `FDO successfully ${formValues.enabled ? 'enabled' : 'disabled'}`);
|
||||
$state.reload();
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Failed applying changes');
|
||||
}
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$async(async () => {
|
||||
try {
|
||||
|
|
|
@ -15,7 +15,6 @@ import { EnvironmentStatus } from '../types';
|
|||
|
||||
import { columns } from './columns';
|
||||
import { EnvironmentListItem } from './types';
|
||||
import { ImportFdoDeviceButton } from './ImportFdoDeviceButton';
|
||||
|
||||
const tableKey = 'environments';
|
||||
const settingsStore = createPersistedStore(tableKey, 'Name');
|
||||
|
@ -83,8 +82,6 @@ export function EnvironmentsDatatable({
|
|||
Remove
|
||||
</Button>
|
||||
|
||||
<ImportFdoDeviceButton />
|
||||
|
||||
{isBE && (
|
||||
<AddButton
|
||||
color="secondary"
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
import { AddButton } from '@@/buttons';
|
||||
|
||||
import { useSettings } from '../../settings/queries';
|
||||
import {
|
||||
FeatureFlag,
|
||||
useFeatureFlag,
|
||||
} from '../../feature-flags/useFeatureFlag';
|
||||
|
||||
export function ImportFdoDeviceButton() {
|
||||
const flagEnabledQuery = useFeatureFlag(FeatureFlag.FDO);
|
||||
|
||||
const isFDOEnabledQuery = useSettings(
|
||||
(settings) => settings.fdoConfiguration.enabled,
|
||||
flagEnabledQuery.data
|
||||
);
|
||||
|
||||
if (!isFDOEnabledQuery.data || !flagEnabledQuery.data) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="ml-[5px]">
|
||||
<AddButton
|
||||
color="secondary"
|
||||
to="portainer.endpoints.importDevice"
|
||||
data-cy="import-fdo-device-button"
|
||||
>
|
||||
Import FDO device
|
||||
</AddButton>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
import { usePublicSettings } from '../settings/queries';
|
||||
|
||||
export enum FeatureFlag {
|
||||
FDO = 'fdo',
|
||||
}
|
||||
export enum FeatureFlag {}
|
||||
|
||||
export function useFeatureFlag(
|
||||
flag: FeatureFlag,
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
import { List } from 'lucide-react';
|
||||
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
import { columns } from './columns';
|
||||
import { FDOProfilesDatatableActions } from './FDOProfilesDatatableActions';
|
||||
import { useFDOProfiles } from './useFDOProfiles';
|
||||
|
||||
const storageKey = 'fdoProfiles';
|
||||
|
||||
const settingsStore = createPersistedStore(storageKey, 'name');
|
||||
|
||||
export interface FDOProfilesDatatableProps {
|
||||
isFDOEnabled: boolean;
|
||||
}
|
||||
|
||||
export function FDOProfilesDatatable({
|
||||
isFDOEnabled,
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
|
||||
const { isLoading, profiles } = useFDOProfiles();
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
columns={columns}
|
||||
dataset={profiles}
|
||||
settingsManager={tableState}
|
||||
title="Device Profiles"
|
||||
titleIcon={List}
|
||||
disableSelect={!isFDOEnabled}
|
||||
getRowId={(row) => row.id.toString()}
|
||||
isLoading={isLoading}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<FDOProfilesDatatableActions
|
||||
isFDOEnabled={isFDOEnabled}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
)}
|
||||
data-cy="fdo-profiles-datatable"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -1,108 +0,0 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import {
|
||||
deleteProfile,
|
||||
duplicateProfile,
|
||||
} from '@/portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
import { confirm } from '@@/modals/confirm';
|
||||
import { Link } from '@@/Link';
|
||||
import { Button } from '@@/buttons';
|
||||
import { DeleteButton } from '@@/buttons/DeleteButton';
|
||||
|
||||
interface Props {
|
||||
isFDOEnabled: boolean;
|
||||
selectedItems: Profile[];
|
||||
}
|
||||
|
||||
export function FDOProfilesDatatableActions({
|
||||
isFDOEnabled,
|
||||
selectedItems,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link
|
||||
to="portainer.endpoints.profile"
|
||||
className="space-left"
|
||||
data-cy="fdo-add-profile-link"
|
||||
>
|
||||
<Button
|
||||
disabled={!isFDOEnabled}
|
||||
icon={PlusCircle}
|
||||
data-cy="fdo-add-profile-button"
|
||||
>
|
||||
Add Profile
|
||||
</Button>
|
||||
</Link>
|
||||
|
||||
<Button
|
||||
disabled={!isFDOEnabled || selectedItems.length !== 1}
|
||||
data-cy="fdo-duplicate-profile-button"
|
||||
onClick={() => onDuplicateProfileClick()}
|
||||
icon={PlusCircle}
|
||||
>
|
||||
Duplicate
|
||||
</Button>
|
||||
|
||||
<DeleteButton
|
||||
disabled={!isFDOEnabled || selectedItems.length === 0}
|
||||
onConfirmed={() => onDeleteProfileClick()}
|
||||
confirmMessage="This action will delete the selected profile(s). Continue?"
|
||||
data-cy="fdo-remove-profile-button"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
async function onDuplicateProfileClick() {
|
||||
const confirmed = await confirm({
|
||||
title: 'Are you sure ?',
|
||||
message: 'This action will duplicate the selected profile. Continue?',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const profile = selectedItems[0];
|
||||
const newProfile = await duplicateProfile(profile.id);
|
||||
notifications.success('Profile successfully duplicated', profile.name);
|
||||
router.stateService.go('portainer.endpoints.profile.edit', {
|
||||
id: newProfile.id,
|
||||
});
|
||||
} catch (err) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to duplicate profile'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function onDeleteProfileClick() {
|
||||
await Promise.all(
|
||||
selectedItems.map(async (profile) => {
|
||||
try {
|
||||
await deleteProfile(profile.id);
|
||||
|
||||
notifications.success('Profile successfully removed', profile.name);
|
||||
} catch (err) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to remove profile'
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
await queryClient.invalidateQueries(['fdo_profiles']);
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const created = columnHelper.accessor('dateCreated', {
|
||||
header: 'Created',
|
||||
id: 'created',
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return isoDateFromTimestamp(value);
|
||||
},
|
||||
});
|
|
@ -1,5 +0,0 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const columnHelper = createColumnHelper<Profile>();
|
|
@ -1,14 +0,0 @@
|
|||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
import { buildNameColumn } from '@@/datatables/buildNameColumn';
|
||||
|
||||
import { created } from './created';
|
||||
|
||||
export const columns = [
|
||||
buildNameColumn<Profile>(
|
||||
'name',
|
||||
'portainer.endpoints.profile.edit',
|
||||
'fdo-profiles-name'
|
||||
),
|
||||
created,
|
||||
];
|
|
@ -1 +0,0 @@
|
|||
export { FDOProfilesDatatable } from './FDOProfilesDatatable';
|
|
@ -1,30 +0,0 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import PortainerError from '@/portainer/error';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { getProfiles } from '@/portainer/hostmanagement/fdo/fdo.service';
|
||||
|
||||
export function useFDOProfiles() {
|
||||
const { isLoading, data, isError, error } = useQuery(['fdo_profiles'], () =>
|
||||
getProfiles()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
error as Error,
|
||||
'Failed retrieving FDO profiles'
|
||||
);
|
||||
}
|
||||
}, [isError, error]);
|
||||
|
||||
const profiles = useMemo(() => data || [], [data]);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
profiles,
|
||||
error: isError ? (error as PortainerError) : undefined,
|
||||
};
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.fdo-table {
|
||||
margin-top: 3em;
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Formik, Field, Form } from 'formik';
|
||||
import { FlaskConical, Laptop } from 'lucide-react';
|
||||
|
||||
import { FDOConfiguration } from '@/portainer/hostmanagement/fdo/model';
|
||||
import {
|
||||
FeatureFlag,
|
||||
useFeatureFlag,
|
||||
} from '@/react/portainer/feature-flags/useFeatureFlag';
|
||||
|
||||
import { Switch } from '@@/form-components/SwitchField/Switch';
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
import { FormSectionTitle } from '@@/form-components/FormSectionTitle';
|
||||
import { Widget, WidgetBody, WidgetTitle } from '@@/Widget';
|
||||
import { LoadingButton } from '@@/buttons/LoadingButton';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
|
||||
import { FDOProfilesDatatable } from '../FDOProfilesDatatable';
|
||||
|
||||
import styles from './SettingsFDO.module.css';
|
||||
import { validationSchema } from './SettingsFDO.validation';
|
||||
|
||||
export interface Settings {
|
||||
fdoConfiguration: FDOConfiguration;
|
||||
EnableEdgeComputeFeatures: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
settings: Settings;
|
||||
onSubmit(values: FDOConfiguration): void;
|
||||
}
|
||||
|
||||
export function SettingsFDO({ settings, onSubmit }: Props) {
|
||||
const flagEnabledQuery = useFeatureFlag(FeatureFlag.FDO);
|
||||
|
||||
if (!flagEnabledQuery.data) {
|
||||
return (
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<TextTip color="blue" icon={FlaskConical}>
|
||||
Since FDO is still an experimental feature that requires additional
|
||||
infrastructure, it has been temporarily hidden in the UI.
|
||||
</TextTip>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
}
|
||||
|
||||
return <SettingsFDOForm settings={settings} onSubmit={onSubmit} />;
|
||||
}
|
||||
|
||||
export function SettingsFDOForm({ settings, onSubmit }: Props) {
|
||||
const fdoConfiguration = settings ? settings.fdoConfiguration : null;
|
||||
const initialFDOEnabled = fdoConfiguration ? fdoConfiguration.enabled : false;
|
||||
|
||||
const [isFDOEnabled, setIsFDOEnabled] = useState(initialFDOEnabled);
|
||||
useEffect(() => {
|
||||
setIsFDOEnabled(settings?.fdoConfiguration?.enabled);
|
||||
}, [settings]);
|
||||
|
||||
const initialValues = {
|
||||
enabled: initialFDOEnabled,
|
||||
ownerURL: fdoConfiguration ? fdoConfiguration.ownerURL : '',
|
||||
ownerUsername: fdoConfiguration ? fdoConfiguration.ownerUsername : '',
|
||||
ownerPassword: fdoConfiguration ? fdoConfiguration.ownerPassword : '',
|
||||
};
|
||||
|
||||
const edgeComputeFeaturesEnabled = settings
|
||||
? settings.EnableEdgeComputeFeatures
|
||||
: false;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<Widget>
|
||||
<WidgetTitle icon={Laptop} title="FDO" />
|
||||
<WidgetBody>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
enableReinitialize
|
||||
validationSchema={() => validationSchema()}
|
||||
validateOnChange
|
||||
validateOnMount
|
||||
>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleSubmit,
|
||||
setFieldValue,
|
||||
isSubmitting,
|
||||
isValid,
|
||||
dirty,
|
||||
}) => (
|
||||
<Form className="form-horizontal" onSubmit={handleSubmit}>
|
||||
<FormControl
|
||||
inputId="edge_enableFDO"
|
||||
label="Enable FDO Management Service"
|
||||
size="small"
|
||||
errors={errors.enabled}
|
||||
>
|
||||
<Switch
|
||||
id="edge_enableFDO"
|
||||
data-cy="edge-enableFDO-switch"
|
||||
name="edge_enableFDO"
|
||||
className="space-right"
|
||||
disabled={!edgeComputeFeaturesEnabled}
|
||||
checked={edgeComputeFeaturesEnabled && values.enabled}
|
||||
onChange={(e) => onChangedEnabled(e, setFieldValue)}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<TextTip color="blue" className="mb-2">
|
||||
When enabled, this will allow Portainer to interact with FDO
|
||||
Services.
|
||||
</TextTip>
|
||||
|
||||
{edgeComputeFeaturesEnabled && values.enabled && (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
<FormControl
|
||||
inputId="owner_url"
|
||||
label="Owner Service Server"
|
||||
errors={errors.ownerURL}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="ownerURL"
|
||||
id="owner_url"
|
||||
placeholder="http://127.0.0.1:8042"
|
||||
value={values.ownerURL}
|
||||
data-cy="fdo-serverInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="owner_username"
|
||||
label="Owner Service Username"
|
||||
errors={errors.ownerUsername}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
name="ownerUsername"
|
||||
id="owner_username"
|
||||
placeholder="username"
|
||||
value={values.ownerUsername}
|
||||
data-cy="fdo-usernameInput"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
inputId="owner_password"
|
||||
label="Owner Service Password"
|
||||
errors={errors.ownerPassword}
|
||||
>
|
||||
<Field
|
||||
as={Input}
|
||||
type="password"
|
||||
name="ownerPassword"
|
||||
id="owner_password"
|
||||
placeholder="password"
|
||||
value={values.ownerPassword}
|
||||
data-cy="fdo-passwordInput"
|
||||
/>
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="form-group mt-5">
|
||||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
data-cy="settings-fdoButton"
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
Save settings
|
||||
</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
|
||||
{edgeComputeFeaturesEnabled && isFDOEnabled && (
|
||||
<div className={styles.fdoTable}>
|
||||
<FormSectionTitle>Device Profiles</FormSectionTitle>
|
||||
<TextTip color="blue" className="mb-2">
|
||||
Add, Edit and Manage the list of device profiles available
|
||||
during FDO device setup
|
||||
</TextTip>
|
||||
<FDOProfilesDatatable isFDOEnabled={initialFDOEnabled} />
|
||||
</div>
|
||||
)}
|
||||
</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
);
|
||||
|
||||
async function onChangedEnabled(
|
||||
e: boolean,
|
||||
setFieldValue: (
|
||||
field: string,
|
||||
value: unknown,
|
||||
shouldValidate?: boolean
|
||||
) => void
|
||||
) {
|
||||
setIsFDOEnabled(e);
|
||||
setFieldValue('enabled', e);
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { object, string } from 'yup';
|
||||
|
||||
export function validationSchema() {
|
||||
return object().shape({
|
||||
ownerURL: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
ownerUsername: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
ownerPassword: string().when('enabled', {
|
||||
is: true,
|
||||
then: string().required('Field is required'),
|
||||
}),
|
||||
});
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export { SettingsFDO } from './SettingsFDO';
|
|
@ -253,7 +253,7 @@ export function SettingsOpenAMT({ settings, onSubmit }: Props) {
|
|||
<div className="col-sm-12">
|
||||
<LoadingButton
|
||||
disabled={!isValid || !dirty}
|
||||
data-cy="settings-fdoButton"
|
||||
data-cy="settings-OpenAMTButton"
|
||||
isLoading={isSubmitting}
|
||||
loadingText="Saving settings..."
|
||||
>
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
|
||||
export interface FDOConfiguration {
|
||||
enabled: boolean;
|
||||
ownerURL: string;
|
||||
ownerUsername: string;
|
||||
ownerPassword: string;
|
||||
}
|
||||
|
||||
export interface TLSConfiguration {
|
||||
TLS: boolean;
|
||||
TLSSkipVerify: boolean;
|
||||
|
@ -119,7 +112,6 @@ export interface Settings {
|
|||
LDAPSettings: LDAPSettings;
|
||||
OAuthSettings: OAuthSettings;
|
||||
openAMTConfiguration: OpenAMTConfiguration;
|
||||
fdoConfiguration: FDOConfiguration;
|
||||
FeatureFlagSettings: { [key: Feature]: boolean };
|
||||
SnapshotInterval: string;
|
||||
TemplatesURL: string;
|
||||
|
@ -201,8 +193,6 @@ export interface PublicSettingsResponse {
|
|||
KubeconfigExpiry: string;
|
||||
/** Whether team sync is enabled */
|
||||
TeamSync: boolean;
|
||||
/** Whether FDO is enabled */
|
||||
IsFDOEnabled: boolean;
|
||||
/** Whether AMT is enabled */
|
||||
IsAMTEnabled: boolean;
|
||||
/** Whether to hide default registry (only on BE) */
|
||||
|
|
Loading…
Reference in New Issue