Compare commits

...

2 Commits

Author SHA1 Message Date
Oscar Zhou
65e4dfa4c0 test(adminmonitor): update the testDatastore options 2022-03-15 11:22:56 +13:00
Oscar Zhou
08d0354371 feat(adminmonitor): disable the app instance if the admin is not initialized in 5 mins 2022-03-14 20:14:04 +13:00
10 changed files with 149 additions and 1 deletions

View File

@@ -41,6 +41,11 @@ func (m *Monitor) Start() {
logFatalf("%s", err)
}
if !initialized {
err = m.DisableInstanceInit()
if err != nil {
log.Printf("[DEBUG] [internal,init] [message: fail to disable initialization monitor: %s]\n", err)
}
logFatalf("[FATAL] [internal,init] No administrator account was created in %f mins. Shutting down the Portainer instance for security reasons", m.timeout.Minutes())
}
case <-cancellationCtx.Done():
@@ -68,3 +73,10 @@ func (m *Monitor) WasInitialized() (bool, error) {
}
return len(users) > 0, nil
}
func (m *Monitor) DisableInstanceInit() error {
state := portainer.State{
DisableAdminInit: true,
}
return m.datastore.State().UpdateState(&state)
}

View File

@@ -33,7 +33,7 @@ func Test_canStopStartedMonitor(t *testing.T) {
func Test_start_shouldFatalAfterTimeout_ifNotInitialized(t *testing.T) {
timeout := 10 * time.Millisecond
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}))
datastore := i.NewDatastore(i.WithUsers([]portainer.User{}), i.WithState(&portainer.State{}))
var fataled bool
origLogFatalf := logFatalf

View File

@@ -61,6 +61,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
MaxBatchSize: kingpin.Flag("max-batch-size", "Maximum size of a batch").Int(),
MaxBatchDelay: kingpin.Flag("max-batch-delay", "Maximum delay before a batch starts").Duration(),
SecretKeyName: kingpin.Flag("secret-key-name", "Secret key name for encryption and will be used as /run/secrets/<secret-key-name>.").Default(defaultSecretKeyName).String(),
EnableAdminInit: kingpin.Flag("enable-admin-init", "Enable Portainer admin initialization ability if the previous initialization expires").Bool(),
}
kingpin.Parse()

View File

@@ -125,6 +125,11 @@ func initDataStore(flags *portainer.CLIFlags, secretKey []byte, fileService port
}
}
err = updateStateFromFlags(store, flags)
if err != nil {
log.Fatalf("Failed updating state from flags: %v", err)
}
err = updateSettingsFromFlags(store, flags)
if err != nil {
log.Fatalf("Failed updating settings from flags: %v", err)
@@ -252,6 +257,24 @@ func initStatus(instanceID string) *portainer.Status {
}
}
func updateStateFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
state, err := dataStore.State().State()
if err != nil {
return err
}
if !state.DisableAdminInit {
return nil
}
if !*flags.EnableAdminInit {
logrus.Fatalln("[FATAL] [internal,init] The Portainer instance initialization is disabled. Restarting the Portainer instance with the optional parameter enable-init")
}
state.DisableAdminInit = !*flags.EnableAdminInit
return dataStore.State().UpdateState(state)
}
func updateSettingsFromFlags(dataStore dataservices.DataStore, flags *portainer.CLIFlags) error {
settings, err := dataStore.Settings().Settings()
if err != nil {

View File

@@ -47,6 +47,7 @@ type (
User() UserService
Version() VersionService
Webhook() WebhookService
State() StateService
}
// CustomTemplateService represents a service to manage custom templates
@@ -298,6 +299,13 @@ type (
DeleteWebhook(ID portainer.WebhookID) error
BucketName() string
}
// State represents a service for managing application state
StateService interface {
State() (*portainer.State, error)
UpdateState(state *portainer.State) error
BucketName() string
}
)
func IsErrObjectNotFound(e error) bool {

View File

@@ -0,0 +1,49 @@
package state
import (
portainer "github.com/portainer/portainer/api"
)
const (
// BucketName represents the name of the bucket where this service stores data.
BucketName = "state"
key = "STATE"
)
// Service represents a service for managing environment(endpoint) data.
type Service struct {
connection portainer.Connection
}
func (service *Service) BucketName() string {
return BucketName
}
// 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{
connection: connection,
}, nil
}
// State retrieve the state object.
func (service *Service) State() (*portainer.State, error) {
var state portainer.State
err := service.connection.GetObject(BucketName, []byte(key), &state)
if err != nil {
return nil, err
}
return &state, nil
}
// UpdateState persists a State object.
func (service *Service) UpdateState(state *portainer.State) error {
return service.connection.UpdateObject(BucketName, []byte(key), state)
}

View File

@@ -27,6 +27,11 @@ func (store *Store) Init() error {
return err
}
err = store.checkOrCreateDefaultState()
if err != nil {
return err
}
return nil
}
@@ -124,3 +129,16 @@ func (store *Store) checkOrCreateDefaultData() error {
}
return nil
}
func (store *Store) checkOrCreateDefaultState() error {
_, err := store.State().State()
if store.IsErrObjectNotFound(err) {
defaultState := &portainer.State{
DisableAdminInit: false,
}
return store.State().UpdateState(defaultState)
}
return err
}

View File

@@ -26,6 +26,7 @@ import (
"github.com/portainer/portainer/api/dataservices/settings"
"github.com/portainer/portainer/api/dataservices/ssl"
"github.com/portainer/portainer/api/dataservices/stack"
"github.com/portainer/portainer/api/dataservices/state"
"github.com/portainer/portainer/api/dataservices/tag"
"github.com/portainer/portainer/api/dataservices/team"
"github.com/portainer/portainer/api/dataservices/teammembership"
@@ -68,6 +69,7 @@ type Store struct {
UserService *user.Service
VersionService *version.Service
WebhookService *webhook.Service
StateService *state.Service
}
func (store *Store) initServices() error {
@@ -227,6 +229,12 @@ func (store *Store) initServices() error {
}
store.ScheduleService = scheduleService
stateService, err := state.NewService(store.connection)
if err != nil {
return err
}
store.StateService = stateService
return nil
}
@@ -345,6 +353,11 @@ func (store *Store) Webhook() dataservices.WebhookService {
return store.WebhookService
}
// State gives access to the State data management layer
func (store *Store) State() dataservices.StateService {
return store.StateService
}
type storeExport struct {
CustomTemplate []portainer.CustomTemplate `json:"customtemplates,omitempty"`
EdgeGroup []portainer.EdgeGroup `json:"edgegroups,omitempty"`

View File

@@ -32,6 +32,7 @@ type testDatastore struct {
user dataservices.UserService
version dataservices.VersionService
webhook dataservices.WebhookService
state dataservices.StateService
}
func (d *testDatastore) BackupTo(io.Writer) error { return nil }
@@ -74,6 +75,7 @@ func (d *testDatastore) TunnelServer() dataservices.TunnelServerService { re
func (d *testDatastore) User() dataservices.UserService { return d.user }
func (d *testDatastore) Version() dataservices.VersionService { return d.version }
func (d *testDatastore) Webhook() dataservices.WebhookService { return d.webhook }
func (d *testDatastore) State() dataservices.StateService { return d.state }
func (d *testDatastore) IsErrObjectNotFound(e error) bool {
return false
@@ -267,3 +269,18 @@ func WithEndpoints(endpoints []portainer.Endpoint) datastoreOption {
d.endpoint = &stubEndpointService{endpoints: endpoints}
}
}
type stubStateService struct {
state *portainer.State
}
func (s *stubStateService) BucketName() string { return "state" }
func (s *stubStateService) State() (*portainer.State, error) { return s.state, nil }
func (s *stubStateService) UpdateState(state *portainer.State) error { return nil }
// WithState testDatastore option that will instruct testDatastore to return provided state
func WithState(s *portainer.State) datastoreOption {
return func(d *testDatastore) {
d.state = &stubStateService{state: s}
}
}

View File

@@ -125,6 +125,7 @@ type (
MaxBatchSize *int
MaxBatchDelay *time.Duration
SecretKeyName *string
EnableAdminInit *bool
}
// CustomTemplate represents a custom template
@@ -1170,6 +1171,12 @@ type (
// WebhookType represents the type of resource a webhook is related to
WebhookType int
// State represents the state of PortainerInstance
State struct {
// Disable the Portainer instance if the initialization set up expires
DisableAdminInit bool `json:"DisableAdminInit" example:"false"`
}
// CLIService represents a service for managing CLI
CLIService interface {
ParseFlags(version string) (*CLIFlags, error)