mirror of
https://github.com/portainer/portainer.git
synced 2025-07-20 13:59:40 +02:00
chore(fdo): remove FDO code EE-7235 (#11981)
This commit is contained in:
parent
1a3db327c7
commit
19fa40286a
57 changed files with 3 additions and 2609 deletions
|
@ -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
|
Endpoint() EndpointService
|
||||||
EndpointGroup() EndpointGroupService
|
EndpointGroup() EndpointGroupService
|
||||||
EndpointRelation() EndpointRelationService
|
EndpointRelation() EndpointRelationService
|
||||||
FDOProfile() FDOProfileService
|
|
||||||
HelmUserRepository() HelmUserRepositoryService
|
HelmUserRepository() HelmUserRepositoryService
|
||||||
Registry() RegistryService
|
Registry() RegistryService
|
||||||
ResourceControl() ResourceControlService
|
ResourceControl() ResourceControlService
|
||||||
|
@ -120,12 +119,6 @@ type (
|
||||||
BucketName() string
|
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 represents a service to manage HelmUserRepositories
|
||||||
HelmUserRepositoryService interface {
|
HelmUserRepositoryService interface {
|
||||||
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||||
"github.com/portainer/portainer/api/dataservices/extension"
|
"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/pendingactions"
|
||||||
"github.com/portainer/portainer/api/dataservices/registry"
|
"github.com/portainer/portainer/api/dataservices/registry"
|
||||||
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
|
||||||
|
@ -41,7 +40,6 @@ type (
|
||||||
endpointService *endpoint.Service
|
endpointService *endpoint.Service
|
||||||
endpointRelationService *endpointrelation.Service
|
endpointRelationService *endpointrelation.Service
|
||||||
extensionService *extension.Service
|
extensionService *extension.Service
|
||||||
fdoProfilesService *fdoprofile.Service
|
|
||||||
registryService *registry.Service
|
registryService *registry.Service
|
||||||
resourceControlService *resourcecontrol.Service
|
resourceControlService *resourcecontrol.Service
|
||||||
roleService *role.Service
|
roleService *role.Service
|
||||||
|
@ -69,7 +67,6 @@ type (
|
||||||
EndpointService *endpoint.Service
|
EndpointService *endpoint.Service
|
||||||
EndpointRelationService *endpointrelation.Service
|
EndpointRelationService *endpointrelation.Service
|
||||||
ExtensionService *extension.Service
|
ExtensionService *extension.Service
|
||||||
FDOProfilesService *fdoprofile.Service
|
|
||||||
RegistryService *registry.Service
|
RegistryService *registry.Service
|
||||||
ResourceControlService *resourcecontrol.Service
|
ResourceControlService *resourcecontrol.Service
|
||||||
RoleService *role.Service
|
RoleService *role.Service
|
||||||
|
@ -99,7 +96,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
||||||
endpointService: parameters.EndpointService,
|
endpointService: parameters.EndpointService,
|
||||||
endpointRelationService: parameters.EndpointRelationService,
|
endpointRelationService: parameters.EndpointRelationService,
|
||||||
extensionService: parameters.ExtensionService,
|
extensionService: parameters.ExtensionService,
|
||||||
fdoProfilesService: parameters.FDOProfilesService,
|
|
||||||
registryService: parameters.RegistryService,
|
registryService: parameters.RegistryService,
|
||||||
resourceControlService: parameters.ResourceControlService,
|
resourceControlService: parameters.ResourceControlService,
|
||||||
roleService: parameters.RoleService,
|
roleService: parameters.RoleService,
|
||||||
|
|
|
@ -17,7 +17,6 @@ import (
|
||||||
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
"github.com/portainer/portainer/api/dataservices/endpointgroup"
|
||||||
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
"github.com/portainer/portainer/api/dataservices/endpointrelation"
|
||||||
"github.com/portainer/portainer/api/dataservices/extension"
|
"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/helmuserrepository"
|
||||||
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
"github.com/portainer/portainer/api/dataservices/pendingactions"
|
||||||
"github.com/portainer/portainer/api/dataservices/registry"
|
"github.com/portainer/portainer/api/dataservices/registry"
|
||||||
|
@ -55,7 +54,6 @@ type Store struct {
|
||||||
EndpointService *endpoint.Service
|
EndpointService *endpoint.Service
|
||||||
EndpointRelationService *endpointrelation.Service
|
EndpointRelationService *endpointrelation.Service
|
||||||
ExtensionService *extension.Service
|
ExtensionService *extension.Service
|
||||||
FDOProfilesService *fdoprofile.Service
|
|
||||||
HelmUserRepositoryService *helmuserrepository.Service
|
HelmUserRepositoryService *helmuserrepository.Service
|
||||||
RegistryService *registry.Service
|
RegistryService *registry.Service
|
||||||
ResourceControlService *resourcecontrol.Service
|
ResourceControlService *resourcecontrol.Service
|
||||||
|
@ -138,12 +136,6 @@ func (store *Store) initServices() error {
|
||||||
}
|
}
|
||||||
store.ExtensionService = extensionService
|
store.ExtensionService = extensionService
|
||||||
|
|
||||||
fdoProfilesService, err := fdoprofile.NewService(store.connection)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
store.FDOProfilesService = fdoProfilesService
|
|
||||||
|
|
||||||
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
|
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -289,11 +281,6 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
|
||||||
return store.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
|
// HelmUserRepository access the helm user repository settings
|
||||||
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
|
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
|
||||||
return store.HelmUserRepositoryService
|
return store.HelmUserRepositoryService
|
||||||
|
|
|
@ -44,7 +44,6 @@ func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
|
||||||
return tx.store.EndpointRelationService.Tx(tx.tx)
|
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) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
|
||||||
|
|
||||||
func (tx *StoreTx) Registry() dataservices.RegistryService {
|
func (tx *StoreTx) Registry() dataservices.RegistryService {
|
||||||
|
|
|
@ -648,12 +648,6 @@
|
||||||
"TemplatesURL": "",
|
"TemplatesURL": "",
|
||||||
"TrustOnFirstConnect": false,
|
"TrustOnFirstConnect": false,
|
||||||
"UserSessionTimeout": "8h",
|
"UserSessionTimeout": "8h",
|
||||||
"fdoConfiguration": {
|
|
||||||
"enabled": false,
|
|
||||||
"ownerPassword": "",
|
|
||||||
"ownerURL": "",
|
|
||||||
"ownerUsername": ""
|
|
||||||
},
|
|
||||||
"openAMTConfiguration": {
|
"openAMTConfiguration": {
|
||||||
"certFileContent": "",
|
"certFileContent": "",
|
||||||
"certFileName": "",
|
"certFileName": "",
|
||||||
|
|
|
@ -36,8 +36,6 @@ const (
|
||||||
ManifestFileDefaultName = "k8s-deployment.yml"
|
ManifestFileDefaultName = "k8s-deployment.yml"
|
||||||
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
|
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
|
||||||
EdgeStackStorePath = "edge_stacks"
|
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 represents the name on disk of the file containing the private key.
|
||||||
PrivateKeyFile = "portainer.key"
|
PrivateKeyFile = "portainer.key"
|
||||||
// PublicKeyFile represents the name on disk of the file containing the public 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)
|
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 {
|
func CreateFile(path string, r io.Reader) error {
|
||||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
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/file"
|
||||||
"github.com/portainer/portainer/api/http/handler/gitops"
|
"github.com/portainer/portainer/api/http/handler/gitops"
|
||||||
"github.com/portainer/portainer/api/http/handler/helm"
|
"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/hostmanagement/openamt"
|
||||||
"github.com/portainer/portainer/api/http/handler/kubernetes"
|
"github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||||
|
@ -69,7 +68,6 @@ type Handler struct {
|
||||||
SettingsHandler *settings.Handler
|
SettingsHandler *settings.Handler
|
||||||
SSLHandler *ssl.Handler
|
SSLHandler *ssl.Handler
|
||||||
OpenAMTHandler *openamt.Handler
|
OpenAMTHandler *openamt.Handler
|
||||||
FDOHandler *fdo.Handler
|
|
||||||
StackHandler *stacks.Handler
|
StackHandler *stacks.Handler
|
||||||
StorybookHandler *storybook.Handler
|
StorybookHandler *storybook.Handler
|
||||||
SystemHandler *system.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)
|
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/open_amt"):
|
case strings.HasPrefix(r.URL.Path, "/api/open_amt"):
|
||||||
http.StripPrefix("/api", h.OpenAMTHandler).ServeHTTP(w, r)
|
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"):
|
case strings.HasPrefix(r.URL.Path, "/api/teams"):
|
||||||
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
||||||
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):
|
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
|
// Whether team sync is enabled
|
||||||
TeamSync bool `json:"TeamSync" example:"true"`
|
TeamSync bool `json:"TeamSync" example:"true"`
|
||||||
|
|
||||||
// Whether FDO is enabled
|
|
||||||
IsFDOEnabled bool
|
|
||||||
// Whether AMT is enabled
|
// Whether AMT is enabled
|
||||||
IsAMTEnabled bool
|
IsAMTEnabled bool
|
||||||
|
|
||||||
|
@ -86,7 +84,6 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
|
||||||
EnableTelemetry: appSettings.EnableTelemetry,
|
EnableTelemetry: appSettings.EnableTelemetry,
|
||||||
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
KubeconfigExpiry: appSettings.KubeconfigExpiry,
|
||||||
Features: featureflags.FeatureFlags(),
|
Features: featureflags.FeatureFlags(),
|
||||||
IsFDOEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.FDOConfiguration.Enabled,
|
|
||||||
IsAMTEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.OpenAMTConfiguration.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/file"
|
||||||
"github.com/portainer/portainer/api/http/handler/gitops"
|
"github.com/portainer/portainer/api/http/handler/gitops"
|
||||||
"github.com/portainer/portainer/api/http/handler/helm"
|
"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/hostmanagement/openamt"
|
||||||
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
|
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
|
||||||
"github.com/portainer/portainer/api/http/handler/ldap"
|
"github.com/portainer/portainer/api/http/handler/ldap"
|
||||||
|
@ -239,8 +238,6 @@ func (server *Server) Start() error {
|
||||||
openAMTHandler.DataStore = server.DataStore
|
openAMTHandler.DataStore = server.DataStore
|
||||||
openAMTHandler.DockerClientFactory = server.DockerClientFactory
|
openAMTHandler.DockerClientFactory = server.DockerClientFactory
|
||||||
|
|
||||||
fdoHandler := fdo.NewHandler(requestBouncer, server.DataStore, server.FileService)
|
|
||||||
|
|
||||||
var stackHandler = stacks.NewHandler(requestBouncer)
|
var stackHandler = stacks.NewHandler(requestBouncer)
|
||||||
stackHandler.DataStore = server.DataStore
|
stackHandler.DataStore = server.DataStore
|
||||||
stackHandler.DockerClientFactory = server.DockerClientFactory
|
stackHandler.DockerClientFactory = server.DockerClientFactory
|
||||||
|
@ -316,7 +313,6 @@ func (server *Server) Start() error {
|
||||||
KubernetesHandler: kubernetesHandler,
|
KubernetesHandler: kubernetesHandler,
|
||||||
MOTDHandler: motdHandler,
|
MOTDHandler: motdHandler,
|
||||||
OpenAMTHandler: openAMTHandler,
|
OpenAMTHandler: openAMTHandler,
|
||||||
FDOHandler: fdoHandler,
|
|
||||||
RegistryHandler: registryHandler,
|
RegistryHandler: registryHandler,
|
||||||
ResourceControlHandler: resourceControlHandler,
|
ResourceControlHandler: resourceControlHandler,
|
||||||
SettingsHandler: settingsHandler,
|
SettingsHandler: settingsHandler,
|
||||||
|
|
|
@ -17,7 +17,6 @@ type testDatastore struct {
|
||||||
endpoint dataservices.EndpointService
|
endpoint dataservices.EndpointService
|
||||||
endpointGroup dataservices.EndpointGroupService
|
endpointGroup dataservices.EndpointGroupService
|
||||||
endpointRelation dataservices.EndpointRelationService
|
endpointRelation dataservices.EndpointRelationService
|
||||||
fdoProfile dataservices.FDOProfileService
|
|
||||||
helmUserRepository dataservices.HelmUserRepositoryService
|
helmUserRepository dataservices.HelmUserRepositoryService
|
||||||
registry dataservices.RegistryService
|
registry dataservices.RegistryService
|
||||||
resourceControl dataservices.ResourceControlService
|
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) Endpoint() dataservices.EndpointService { return d.endpoint }
|
||||||
func (d *testDatastore) EndpointGroup() dataservices.EndpointGroupService { return d.endpointGroup }
|
func (d *testDatastore) EndpointGroup() dataservices.EndpointGroupService { return d.endpointGroup }
|
||||||
|
|
||||||
func (d *testDatastore) FDOProfile() dataservices.FDOProfileService {
|
|
||||||
return d.fdoProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *testDatastore) EndpointRelation() dataservices.EndpointRelationService {
|
func (d *testDatastore) EndpointRelation() dataservices.EndpointRelationService {
|
||||||
return d.endpointRelation
|
return d.endpointRelation
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,24 +96,6 @@ type (
|
||||||
// PowerState represents an AMT managed device power state
|
// PowerState represents an AMT managed device power state
|
||||||
PowerState int
|
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 represents the available flags on the CLI
|
||||||
CLIFlags struct {
|
CLIFlags struct {
|
||||||
Addr *string
|
Addr *string
|
||||||
|
@ -963,7 +945,6 @@ type (
|
||||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||||
OpenAMTConfiguration OpenAMTConfiguration `json:"openAMTConfiguration"`
|
OpenAMTConfiguration OpenAMTConfiguration `json:"openAMTConfiguration"`
|
||||||
FDOConfiguration FDOConfiguration `json:"fdoConfiguration"`
|
|
||||||
FeatureFlagSettings map[featureflags.Feature]bool `json:"FeatureFlagSettings"`
|
FeatureFlagSettings map[featureflags.Feature]bool `json:"FeatureFlagSettings"`
|
||||||
// The interval in which environment(endpoint) snapshots are created
|
// The interval in which environment(endpoint) snapshots are created
|
||||||
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
|
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
|
||||||
|
@ -1456,7 +1437,6 @@ type (
|
||||||
StoreSSLCertPair(cert, key []byte) (string, string, error)
|
StoreSSLCertPair(cert, key []byte) (string, string, error)
|
||||||
CopySSLCertPair(certPath, keyPath string) (string, string, error)
|
CopySSLCertPair(certPath, keyPath string) (string, string, error)
|
||||||
CopySSLCACert(caCertPath string) (string, error)
|
CopySSLCACert(caCertPath string) (string, error)
|
||||||
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
|
|
||||||
StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error)
|
StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error)
|
||||||
GetDefaultChiselPrivateKeyPath() string
|
GetDefaultChiselPrivateKeyPath() string
|
||||||
StoreChiselPrivateKey(privateKey []byte) error
|
StoreChiselPrivateKey(privateKey []byte) error
|
||||||
|
@ -1646,13 +1626,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of supported features
|
// List of supported features
|
||||||
const (
|
var SupportedFeatureFlags = []featureflags.Feature{}
|
||||||
FeatureFdo = "fdo"
|
|
||||||
)
|
|
||||||
|
|
||||||
var SupportedFeatureFlags = []featureflags.Feature{
|
|
||||||
FeatureFdo,
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
_ AuthenticationMethod = iota
|
_ 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 = {
|
const edgeAutoCreateScript = {
|
||||||
name: 'portainer.endpoints.edgeAutoCreateScript',
|
name: 'portainer.endpoints.edgeAutoCreateScript',
|
||||||
url: '/aeec',
|
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 = {
|
var endpointAccess = {
|
||||||
name: 'portainer.endpoints.endpoint.access',
|
name: 'portainer.endpoints.endpoint.access',
|
||||||
url: '/access',
|
url: '/access',
|
||||||
|
@ -484,9 +453,6 @@ angular
|
||||||
$stateRegistryProvider.register(endpointAccess);
|
$stateRegistryProvider.register(endpointAccess);
|
||||||
$stateRegistryProvider.register(endpointKVM);
|
$stateRegistryProvider.register(endpointKVM);
|
||||||
$stateRegistryProvider.register(edgeAutoCreateScript);
|
$stateRegistryProvider.register(edgeAutoCreateScript);
|
||||||
$stateRegistryProvider.register(deviceImport);
|
|
||||||
$stateRegistryProvider.register(addFDOProfile);
|
|
||||||
$stateRegistryProvider.register(editFDOProfile);
|
|
||||||
$stateRegistryProvider.register(groups);
|
$stateRegistryProvider.register(groups);
|
||||||
$stateRegistryProvider.register(group);
|
$stateRegistryProvider.register(group);
|
||||||
$stateRegistryProvider.register(groupAccess);
|
$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.LDAPSettings = data.LDAPSettings;
|
||||||
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
|
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
|
||||||
this.openAMTConfiguration = data.openAMTConfiguration;
|
this.openAMTConfiguration = data.openAMTConfiguration;
|
||||||
this.fdoConfiguration = data.fdoConfiguration;
|
|
||||||
this.SnapshotInterval = data.SnapshotInterval;
|
this.SnapshotInterval = data.SnapshotInterval;
|
||||||
this.TemplatesURL = data.TemplatesURL;
|
this.TemplatesURL = data.TemplatesURL;
|
||||||
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
|
||||||
|
@ -37,7 +36,6 @@ export function PublicSettingsViewModel(settings) {
|
||||||
this.Edge = new EdgeSettingsViewModel(settings.Edge);
|
this.Edge = new EdgeSettingsViewModel(settings.Edge);
|
||||||
this.DefaultRegistry = settings.DefaultRegistry;
|
this.DefaultRegistry = settings.DefaultRegistry;
|
||||||
this.IsAMTEnabled = settings.IsAMTEnabled;
|
this.IsAMTEnabled = settings.IsAMTEnabled;
|
||||||
this.IsFDOEnabled = settings.IsFDOEnabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function InternalAuthSettingsViewModel(data) {
|
export function InternalAuthSettingsViewModel(data) {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { SettingsFDO } from '@/react/portainer/settings/EdgeComputeView/SettingsFDO';
|
|
||||||
import { SettingsOpenAMT } from '@/react/portainer/settings/EdgeComputeView/SettingsOpenAMT';
|
import { SettingsOpenAMT } from '@/react/portainer/settings/EdgeComputeView/SettingsOpenAMT';
|
||||||
import { InternalAuth } from '@/react/portainer/settings/AuthenticationView/InternalAuth';
|
import { InternalAuth } from '@/react/portainer/settings/AuthenticationView/InternalAuth';
|
||||||
import { r2a } from '@/react-tools/react2angular';
|
import { r2a } from '@/react-tools/react2angular';
|
||||||
|
@ -17,10 +16,6 @@ import { AuthStyleField } from '@/react/portainer/settings/AuthenticationView/OA
|
||||||
|
|
||||||
export const settingsModule = angular
|
export const settingsModule = angular
|
||||||
.module('portainer.app.react.components.settings', [])
|
.module('portainer.app.react.components.settings', [])
|
||||||
.component(
|
|
||||||
'settingsFdo',
|
|
||||||
r2a(withUIRouter(withReactQuery(SettingsFDO)), ['onSubmit', 'settings'])
|
|
||||||
)
|
|
||||||
.component('settingsOpenAmt', r2a(SettingsOpenAMT, ['onSubmit', 'settings']))
|
.component('settingsOpenAmt', r2a(SettingsOpenAMT, ['onSubmit', 'settings']))
|
||||||
.component(
|
.component(
|
||||||
'internalAuth',
|
'internalAuth',
|
||||||
|
|
|
@ -195,15 +195,5 @@ function FileUploadFactory($q, Upload) {
|
||||||
return $q.all(queue);
|
return $q.all(queue);
|
||||||
};
|
};
|
||||||
|
|
||||||
service.uploadOwnershipVoucher = function (voucherFile) {
|
|
||||||
return Upload.upload({
|
|
||||||
url: 'api/fdo/register',
|
|
||||||
data: {
|
|
||||||
voucher: voucherFile,
|
|
||||||
},
|
|
||||||
ignoreLoadingBar: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return service;
|
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>
|
<settings-open-amt on-submit="($ctrl.onSubmitOpenAMT)" settings="($ctrl.settings)"></settings-open-amt>
|
||||||
</div>
|
</div>
|
||||||
</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 _ from 'lodash-es';
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { configureFDO } from '@/portainer/hostmanagement/fdo/fdo.service';
|
|
||||||
import { configureAMT } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
import { configureAMT } from 'Portainer/hostmanagement/open-amt/open-amt.service';
|
||||||
|
|
||||||
angular.module('portainer.app').controller('SettingsEdgeComputeController', SettingsEdgeComputeController);
|
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() {
|
function initView() {
|
||||||
$async(async () => {
|
$async(async () => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -15,7 +15,6 @@ import { EnvironmentStatus } from '../types';
|
||||||
|
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
import { EnvironmentListItem } from './types';
|
import { EnvironmentListItem } from './types';
|
||||||
import { ImportFdoDeviceButton } from './ImportFdoDeviceButton';
|
|
||||||
|
|
||||||
const tableKey = 'environments';
|
const tableKey = 'environments';
|
||||||
const settingsStore = createPersistedStore(tableKey, 'Name');
|
const settingsStore = createPersistedStore(tableKey, 'Name');
|
||||||
|
@ -83,8 +82,6 @@ export function EnvironmentsDatatable({
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ImportFdoDeviceButton />
|
|
||||||
|
|
||||||
{isBE && (
|
{isBE && (
|
||||||
<AddButton
|
<AddButton
|
||||||
color="secondary"
|
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';
|
import { usePublicSettings } from '../settings/queries';
|
||||||
|
|
||||||
export enum FeatureFlag {
|
export enum FeatureFlag {}
|
||||||
FDO = 'fdo',
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useFeatureFlag(
|
export function useFeatureFlag(
|
||||||
flag: FeatureFlag,
|
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">
|
<div className="col-sm-12">
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
disabled={!isValid || !dirty}
|
disabled={!isValid || !dirty}
|
||||||
data-cy="settings-fdoButton"
|
data-cy="settings-OpenAMTButton"
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
loadingText="Saving settings..."
|
loadingText="Saving settings..."
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||||
|
|
||||||
export interface FDOConfiguration {
|
|
||||||
enabled: boolean;
|
|
||||||
ownerURL: string;
|
|
||||||
ownerUsername: string;
|
|
||||||
ownerPassword: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TLSConfiguration {
|
export interface TLSConfiguration {
|
||||||
TLS: boolean;
|
TLS: boolean;
|
||||||
TLSSkipVerify: boolean;
|
TLSSkipVerify: boolean;
|
||||||
|
@ -119,7 +112,6 @@ export interface Settings {
|
||||||
LDAPSettings: LDAPSettings;
|
LDAPSettings: LDAPSettings;
|
||||||
OAuthSettings: OAuthSettings;
|
OAuthSettings: OAuthSettings;
|
||||||
openAMTConfiguration: OpenAMTConfiguration;
|
openAMTConfiguration: OpenAMTConfiguration;
|
||||||
fdoConfiguration: FDOConfiguration;
|
|
||||||
FeatureFlagSettings: { [key: Feature]: boolean };
|
FeatureFlagSettings: { [key: Feature]: boolean };
|
||||||
SnapshotInterval: string;
|
SnapshotInterval: string;
|
||||||
TemplatesURL: string;
|
TemplatesURL: string;
|
||||||
|
@ -201,8 +193,6 @@ export interface PublicSettingsResponse {
|
||||||
KubeconfigExpiry: string;
|
KubeconfigExpiry: string;
|
||||||
/** Whether team sync is enabled */
|
/** Whether team sync is enabled */
|
||||||
TeamSync: boolean;
|
TeamSync: boolean;
|
||||||
/** Whether FDO is enabled */
|
|
||||||
IsFDOEnabled: boolean;
|
|
||||||
/** Whether AMT is enabled */
|
/** Whether AMT is enabled */
|
||||||
IsAMTEnabled: boolean;
|
IsAMTEnabled: boolean;
|
||||||
/** Whether to hide default registry (only on BE) */
|
/** Whether to hide default registry (only on BE) */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue