chore(fdo): remove FDO code EE-7235 (#11981)

pull/11984/head
andres-portainer 2024-06-28 08:42:16 -03:00 committed by GitHub
parent 1a3db327c7
commit 19fa40286a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
57 changed files with 3 additions and 2609 deletions

View File

@ -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)
}

View File

@ -15,7 +15,6 @@ type (
Endpoint() EndpointService
EndpointGroup() EndpointGroupService
EndpointRelation() EndpointRelationService
FDOProfile() FDOProfileService
HelmUserRepository() HelmUserRepositoryService
Registry() RegistryService
ResourceControl() ResourceControlService
@ -120,12 +119,6 @@ type (
BucketName() string
}
// FDOProfileService represents a service to manage FDO Profiles
FDOProfileService interface {
BaseCRUD[portainer.FDOProfile, portainer.FDOProfileID]
GetNextIdentifier() int
}
// HelmUserRepositoryService represents a service to manage HelmUserRepositories
HelmUserRepositoryService interface {
BaseCRUD[portainer.HelmUserRepository, portainer.HelmUserRepositoryID]

View File

@ -13,7 +13,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/pendingactions"
"github.com/portainer/portainer/api/dataservices/registry"
"github.com/portainer/portainer/api/dataservices/resourcecontrol"
@ -41,7 +40,6 @@ type (
endpointService *endpoint.Service
endpointRelationService *endpointrelation.Service
extensionService *extension.Service
fdoProfilesService *fdoprofile.Service
registryService *registry.Service
resourceControlService *resourcecontrol.Service
roleService *role.Service
@ -69,7 +67,6 @@ type (
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
RoleService *role.Service
@ -99,7 +96,6 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
endpointService: parameters.EndpointService,
endpointRelationService: parameters.EndpointRelationService,
extensionService: parameters.ExtensionService,
fdoProfilesService: parameters.FDOProfilesService,
registryService: parameters.RegistryService,
resourceControlService: parameters.ResourceControlService,
roleService: parameters.RoleService,

View File

@ -17,7 +17,6 @@ import (
"github.com/portainer/portainer/api/dataservices/endpointgroup"
"github.com/portainer/portainer/api/dataservices/endpointrelation"
"github.com/portainer/portainer/api/dataservices/extension"
"github.com/portainer/portainer/api/dataservices/fdoprofile"
"github.com/portainer/portainer/api/dataservices/helmuserrepository"
"github.com/portainer/portainer/api/dataservices/pendingactions"
"github.com/portainer/portainer/api/dataservices/registry"
@ -55,7 +54,6 @@ type Store struct {
EndpointService *endpoint.Service
EndpointRelationService *endpointrelation.Service
ExtensionService *extension.Service
FDOProfilesService *fdoprofile.Service
HelmUserRepositoryService *helmuserrepository.Service
RegistryService *registry.Service
ResourceControlService *resourcecontrol.Service
@ -138,12 +136,6 @@ func (store *Store) initServices() error {
}
store.ExtensionService = extensionService
fdoProfilesService, err := fdoprofile.NewService(store.connection)
if err != nil {
return err
}
store.FDOProfilesService = fdoProfilesService
helmUserRepositoryService, err := helmuserrepository.NewService(store.connection)
if err != nil {
return err
@ -289,11 +281,6 @@ func (store *Store) EndpointRelation() dataservices.EndpointRelationService {
return store.EndpointRelationService
}
// FDOProfile gives access to the FDOProfile data management layer
func (store *Store) FDOProfile() dataservices.FDOProfileService {
return store.FDOProfilesService
}
// HelmUserRepository access the helm user repository settings
func (store *Store) HelmUserRepository() dataservices.HelmUserRepositoryService {
return store.HelmUserRepositoryService

View File

@ -44,7 +44,6 @@ func (tx *StoreTx) EndpointRelation() dataservices.EndpointRelationService {
return tx.store.EndpointRelationService.Tx(tx.tx)
}
func (tx *StoreTx) FDOProfile() dataservices.FDOProfileService { return nil }
func (tx *StoreTx) HelmUserRepository() dataservices.HelmUserRepositoryService { return nil }
func (tx *StoreTx) Registry() dataservices.RegistryService {

View File

@ -648,12 +648,6 @@
"TemplatesURL": "",
"TrustOnFirstConnect": false,
"UserSessionTimeout": "8h",
"fdoConfiguration": {
"enabled": false,
"ownerPassword": "",
"ownerURL": "",
"ownerUsername": ""
},
"openAMTConfiguration": {
"certFileContent": "",
"certFileName": "",

View File

@ -36,8 +36,6 @@ const (
ManifestFileDefaultName = "k8s-deployment.yml"
// EdgeStackStorePath represents the subfolder where edge stack files are stored in the file store folder.
EdgeStackStorePath = "edge_stacks"
// FDOProfileStorePath represents the subfolder where FDO profiles files are stored in the file store folder.
FDOProfileStorePath = "fdo_profiles"
// PrivateKeyFile represents the name on disk of the file containing the private key.
PrivateKeyFile = "portainer.key"
// PublicKeyFile represents the name on disk of the file containing the public key.
@ -1003,23 +1001,6 @@ func MoveDirectory(originalPath, newPath string, overwriteTargetPath bool) error
return os.Rename(originalPath, newPath)
}
// StoreFDOProfileFileFromBytes creates a subfolder in the FDOProfileStorePath and stores a new file from bytes.
// It returns the path to the folder where the file is stored.
func (service *Service) StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error) {
err := service.createDirectoryInStore(FDOProfileStorePath)
if err != nil {
return "", err
}
filePath := JoinPaths(FDOProfileStorePath, fdoProfileIdentifier)
err = service.createFileInStore(filePath, bytes.NewReader(data))
if err != nil {
return "", err
}
return service.wrapFileStore(filePath), nil
}
func CreateFile(path string, r io.Reader) error {
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {

View File

@ -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
}

View File

@ -19,7 +19,6 @@ import (
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/gitops"
"github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
"github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
@ -69,7 +68,6 @@ type Handler struct {
SettingsHandler *settings.Handler
SSLHandler *ssl.Handler
OpenAMTHandler *openamt.Handler
FDOHandler *fdo.Handler
StackHandler *stacks.Handler
StorybookHandler *storybook.Handler
SystemHandler *system.Handler
@ -252,8 +250,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.StripPrefix("/api", h.SSLHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/open_amt"):
http.StripPrefix("/api", h.OpenAMTHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/fdo"):
http.StripPrefix("/api", h.FDOHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/teams"):
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):

View File

@ -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)
}

View File

@ -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}
`

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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),
})
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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})
}

View File

@ -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
}

View File

@ -36,8 +36,6 @@ type publicSettingsResponse struct {
// Whether team sync is enabled
TeamSync bool `json:"TeamSync" example:"true"`
// Whether FDO is enabled
IsFDOEnabled bool
// Whether AMT is enabled
IsAMTEnabled bool
@ -86,7 +84,6 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp
EnableTelemetry: appSettings.EnableTelemetry,
KubeconfigExpiry: appSettings.KubeconfigExpiry,
Features: featureflags.FeatureFlags(),
IsFDOEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.FDOConfiguration.Enabled,
IsAMTEnabled: appSettings.EnableEdgeComputeFeatures && appSettings.OpenAMTConfiguration.Enabled,
}

View File

@ -32,7 +32,6 @@ import (
"github.com/portainer/portainer/api/http/handler/file"
"github.com/portainer/portainer/api/http/handler/gitops"
"github.com/portainer/portainer/api/http/handler/helm"
"github.com/portainer/portainer/api/http/handler/hostmanagement/fdo"
"github.com/portainer/portainer/api/http/handler/hostmanagement/openamt"
kubehandler "github.com/portainer/portainer/api/http/handler/kubernetes"
"github.com/portainer/portainer/api/http/handler/ldap"
@ -239,8 +238,6 @@ func (server *Server) Start() error {
openAMTHandler.DataStore = server.DataStore
openAMTHandler.DockerClientFactory = server.DockerClientFactory
fdoHandler := fdo.NewHandler(requestBouncer, server.DataStore, server.FileService)
var stackHandler = stacks.NewHandler(requestBouncer)
stackHandler.DataStore = server.DataStore
stackHandler.DockerClientFactory = server.DockerClientFactory
@ -316,7 +313,6 @@ func (server *Server) Start() error {
KubernetesHandler: kubernetesHandler,
MOTDHandler: motdHandler,
OpenAMTHandler: openAMTHandler,
FDOHandler: fdoHandler,
RegistryHandler: registryHandler,
ResourceControlHandler: resourceControlHandler,
SettingsHandler: settingsHandler,

View File

@ -17,7 +17,6 @@ type testDatastore struct {
endpoint dataservices.EndpointService
endpointGroup dataservices.EndpointGroupService
endpointRelation dataservices.EndpointRelationService
fdoProfile dataservices.FDOProfileService
helmUserRepository dataservices.HelmUserRepositoryService
registry dataservices.RegistryService
resourceControl dataservices.ResourceControlService
@ -55,10 +54,6 @@ func (d *testDatastore) EdgeStack() dataservices.EdgeStackService { re
func (d *testDatastore) Endpoint() dataservices.EndpointService { return d.endpoint }
func (d *testDatastore) EndpointGroup() dataservices.EndpointGroupService { return d.endpointGroup }
func (d *testDatastore) FDOProfile() dataservices.FDOProfileService {
return d.fdoProfile
}
func (d *testDatastore) EndpointRelation() dataservices.EndpointRelationService {
return d.endpointRelation
}

View File

@ -96,24 +96,6 @@ type (
// PowerState represents an AMT managed device power state
PowerState int
FDOConfiguration struct {
Enabled bool `json:"enabled"`
OwnerURL string `json:"ownerURL"`
OwnerUsername string `json:"ownerUsername"`
OwnerPassword string `json:"ownerPassword"`
}
// FDOProfileID represents a fdo profile id
FDOProfileID int
FDOProfile struct {
ID FDOProfileID `json:"id"`
Name string `json:"name"`
FilePath string `json:"filePath"`
NumberDevices int `json:"numberDevices"`
DateCreated int64 `json:"dateCreated"`
}
// CLIFlags represents the available flags on the CLI
CLIFlags struct {
Addr *string
@ -963,7 +945,6 @@ type (
LDAPSettings LDAPSettings `json:"LDAPSettings"`
OAuthSettings OAuthSettings `json:"OAuthSettings"`
OpenAMTConfiguration OpenAMTConfiguration `json:"openAMTConfiguration"`
FDOConfiguration FDOConfiguration `json:"fdoConfiguration"`
FeatureFlagSettings map[featureflags.Feature]bool `json:"FeatureFlagSettings"`
// The interval in which environment(endpoint) snapshots are created
SnapshotInterval string `json:"SnapshotInterval" example:"5m"`
@ -1456,7 +1437,6 @@ type (
StoreSSLCertPair(cert, key []byte) (string, string, error)
CopySSLCertPair(certPath, keyPath string) (string, string, error)
CopySSLCACert(caCertPath string) (string, error)
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
StoreMTLSCertificates(cert, caCert, key []byte) (string, string, string, error)
GetDefaultChiselPrivateKeyPath() string
StoreChiselPrivateKey(privateKey []byte) error
@ -1646,13 +1626,7 @@ const (
)
// List of supported features
const (
FeatureFdo = "fdo"
)
var SupportedFeatureFlags = []featureflags.Feature{
FeatureFdo,
}
var SupportedFeatureFlags = []featureflags.Feature{}
const (
_ AuthenticationMethod = iota

View File

@ -200,17 +200,6 @@ angular
},
};
var deviceImport = {
name: 'portainer.endpoints.importDevice',
url: '/device',
views: {
'content@': {
templateUrl: './views/devices/import/importDevice.html',
controller: 'ImportDeviceController',
},
},
};
const edgeAutoCreateScript = {
name: 'portainer.endpoints.edgeAutoCreateScript',
url: '/aeec',
@ -224,26 +213,6 @@ angular
},
};
var addFDOProfile = {
name: 'portainer.endpoints.profile',
url: '/profile',
views: {
'content@': {
component: 'addProfileView',
},
},
};
var editFDOProfile = {
name: 'portainer.endpoints.profile.edit',
url: '/:id',
views: {
'content@': {
component: 'editProfileView',
},
},
};
var endpointAccess = {
name: 'portainer.endpoints.endpoint.access',
url: '/access',
@ -484,9 +453,6 @@ angular
$stateRegistryProvider.register(endpointAccess);
$stateRegistryProvider.register(endpointKVM);
$stateRegistryProvider.register(edgeAutoCreateScript);
$stateRegistryProvider.register(deviceImport);
$stateRegistryProvider.register(addFDOProfile);
$stateRegistryProvider.register(editFDOProfile);
$stateRegistryProvider.register(groups);
$stateRegistryProvider.register(group);
$stateRegistryProvider.register(groupAccess);

View File

@ -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');
}
}

View File

@ -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;
};

View File

@ -6,7 +6,6 @@ export function SettingsViewModel(data) {
this.LDAPSettings = data.LDAPSettings;
this.OAuthSettings = new OAuthSettingsViewModel(data.OAuthSettings);
this.openAMTConfiguration = data.openAMTConfiguration;
this.fdoConfiguration = data.fdoConfiguration;
this.SnapshotInterval = data.SnapshotInterval;
this.TemplatesURL = data.TemplatesURL;
this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval;
@ -37,7 +36,6 @@ export function PublicSettingsViewModel(settings) {
this.Edge = new EdgeSettingsViewModel(settings.Edge);
this.DefaultRegistry = settings.DefaultRegistry;
this.IsAMTEnabled = settings.IsAMTEnabled;
this.IsFDOEnabled = settings.IsFDOEnabled;
}
export function InternalAuthSettingsViewModel(data) {

View File

@ -1,6 +1,5 @@
import angular from 'angular';
import { SettingsFDO } from '@/react/portainer/settings/EdgeComputeView/SettingsFDO';
import { SettingsOpenAMT } from '@/react/portainer/settings/EdgeComputeView/SettingsOpenAMT';
import { InternalAuth } from '@/react/portainer/settings/AuthenticationView/InternalAuth';
import { r2a } from '@/react-tools/react2angular';
@ -17,10 +16,6 @@ import { AuthStyleField } from '@/react/portainer/settings/AuthenticationView/OA
export const settingsModule = angular
.module('portainer.app.react.components.settings', [])
.component(
'settingsFdo',
r2a(withUIRouter(withReactQuery(SettingsFDO)), ['onSubmit', 'settings'])
)
.component('settingsOpenAmt', r2a(SettingsOpenAMT, ['onSubmit', 'settings']))
.component(
'internalAuth',

View File

@ -195,15 +195,5 @@ function FileUploadFactory($q, Upload) {
return $q.all(queue);
};
service.uploadOwnershipVoucher = function (voucherFile) {
return Upload.upload({
url: 'api/fdo/register',
data: {
voucher: voucherFile,
},
ignoreLoadingBar: true,
});
};
return service;
}

View File

@ -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>

View File

@ -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();
}
);

View File

@ -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>

View File

@ -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;
}
}

View File

@ -1,8 +0,0 @@
import angular from 'angular';
import controller from './addProfileController';
angular.module('portainer.app').component('addProfileView', {
templateUrl: './addProfile.html',
controller,
});

View File

@ -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>

View File

@ -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();
}

View File

@ -1,8 +0,0 @@
import angular from 'angular';
import controller from './editProfileController';
angular.module('portainer.app').component('editProfileView', {
templateUrl: './editProfile.html',
controller,
});

View File

@ -11,9 +11,3 @@
<settings-open-amt on-submit="($ctrl.onSubmitOpenAMT)" settings="($ctrl.settings)"></settings-open-amt>
</div>
</div>
<div class="row">
<div class="col-sm-12" ng-if="$ctrl.settings">
<settings-fdo on-submit="($ctrl.onSubmitFDO)" settings="($ctrl.settings)"></settings-fdo>
</div>
</div>

View File

@ -1,7 +1,6 @@
import _ from 'lodash-es';
import angular from 'angular';
import { configureFDO } from '@/portainer/hostmanagement/fdo/fdo.service';
import { configureAMT } from 'Portainer/hostmanagement/open-amt/open-amt.service';
angular.module('portainer.app').controller('SettingsEdgeComputeController', SettingsEdgeComputeController);
@ -31,16 +30,6 @@ export default function SettingsEdgeComputeController($q, $async, $state, Notifi
}
};
this.onSubmitFDO = async function (formValues) {
try {
await configureFDO(formValues);
Notifications.success('Success', `FDO successfully ${formValues.enabled ? 'enabled' : 'disabled'}`);
$state.reload();
} catch (err) {
Notifications.error('Failure', err, 'Failed applying changes');
}
};
function initView() {
$async(async () => {
try {

View File

@ -15,7 +15,6 @@ import { EnvironmentStatus } from '../types';
import { columns } from './columns';
import { EnvironmentListItem } from './types';
import { ImportFdoDeviceButton } from './ImportFdoDeviceButton';
const tableKey = 'environments';
const settingsStore = createPersistedStore(tableKey, 'Name');
@ -83,8 +82,6 @@ export function EnvironmentsDatatable({
Remove
</Button>
<ImportFdoDeviceButton />
{isBE && (
<AddButton
color="secondary"

View File

@ -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>
);
}

View File

@ -1,8 +1,6 @@
import { usePublicSettings } from '../settings/queries';
export enum FeatureFlag {
FDO = 'fdo',
}
export enum FeatureFlag {}
export function useFeatureFlag(
flag: FeatureFlag,

View File

@ -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"
/>
);
}

View File

@ -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']);
}
}

View File

@ -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);
},
});

View File

@ -1,5 +0,0 @@
import { createColumnHelper } from '@tanstack/react-table';
import { Profile } from '@/portainer/hostmanagement/fdo/model';
export const columnHelper = createColumnHelper<Profile>();

View File

@ -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,
];

View File

@ -1 +0,0 @@
export { FDOProfilesDatatable } from './FDOProfilesDatatable';

View File

@ -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,
};
}

View File

@ -1,3 +0,0 @@
.fdo-table {
margin-top: 3em;
}

View File

@ -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);
}
}

View File

@ -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'),
}),
});
}

View File

@ -1 +0,0 @@
export { SettingsFDO } from './SettingsFDO';

View File

@ -253,7 +253,7 @@ export function SettingsOpenAMT({ settings, onSubmit }: Props) {
<div className="col-sm-12">
<LoadingButton
disabled={!isValid || !dirty}
data-cy="settings-fdoButton"
data-cy="settings-OpenAMTButton"
isLoading={isSubmitting}
loadingText="Saving settings..."
>

View File

@ -1,12 +1,5 @@
import { TeamId } from '@/react/portainer/users/teams/types';
export interface FDOConfiguration {
enabled: boolean;
ownerURL: string;
ownerUsername: string;
ownerPassword: string;
}
export interface TLSConfiguration {
TLS: boolean;
TLSSkipVerify: boolean;
@ -119,7 +112,6 @@ export interface Settings {
LDAPSettings: LDAPSettings;
OAuthSettings: OAuthSettings;
openAMTConfiguration: OpenAMTConfiguration;
fdoConfiguration: FDOConfiguration;
FeatureFlagSettings: { [key: Feature]: boolean };
SnapshotInterval: string;
TemplatesURL: string;
@ -201,8 +193,6 @@ export interface PublicSettingsResponse {
KubeconfigExpiry: string;
/** Whether team sync is enabled */
TeamSync: boolean;
/** Whether FDO is enabled */
IsFDOEnabled: boolean;
/** Whether AMT is enabled */
IsAMTEnabled: boolean;
/** Whether to hide default registry (only on BE) */