eb35e9c47f
Add an opt-in webhook notification for container-automation events (image
update, rollback, update-failed, auto-heal restart), plugging into the existing
Notifier seam in notify.go.
- Settings: new ContainerAutomation.Notification.WebhookURL (shared across
update + heal), persisted and validated in the settings update handler
(optional; http/https only; accepts the {{message}} placeholder).
- webhookNotifier reads the current URL from the datastore per event (UI changes
take effect without a restart). If the URL contains {{message}} it substitutes
the URL-encoded message and issues a GET; otherwise it POSTs the message as the
body. Delivery, the env/stack name lookups, and any panic run in a goroutine
under recover() with a 10s timeout — strictly best-effort, never blocks or
crashes the automation daemon. multiNotifier fans events to logNotifier +
webhook and isolates a panic in any one notifier.
- Message format (maintainer's spec):
Environment | <env>
Stack [<name>] (Container [<name>] for non-stack events)
Update [<name>]: <old> -> <new>
Auto-heal: 'Auto-heal: restarted unhealthy container'.
- New NotificationPanel in settings to configure the URL.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
130 lines
3.7 KiB
Go
130 lines
3.7 KiB
Go
package datastore
|
|
|
|
import (
|
|
"os"
|
|
|
|
portainer "github.com/portainer/portainer/api"
|
|
)
|
|
|
|
// Init creates the default data set.
|
|
func (store *Store) Init() error {
|
|
err := store.checkOrCreateDefaultSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = store.checkOrCreateDefaultSSLSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return store.checkOrCreateDefaultData()
|
|
}
|
|
|
|
func (store *Store) checkOrCreateDefaultSettings() error {
|
|
isDDExtention := false
|
|
if _, ok := os.LookupEnv("DOCKER_EXTENSION"); ok {
|
|
isDDExtention = true
|
|
}
|
|
|
|
// TODO: these need to also be applied when importing
|
|
settings, err := store.SettingsService.Settings()
|
|
if store.IsErrObjectNotFound(err) {
|
|
defaultSettings := &portainer.Settings{
|
|
AuthenticationMethod: portainer.AuthenticationInternal,
|
|
BlackListedLabels: make([]portainer.Pair, 0),
|
|
InternalAuthSettings: portainer.InternalAuthSettings{
|
|
RequiredPasswordLength: 12,
|
|
},
|
|
LDAPSettings: portainer.LDAPSettings{
|
|
AnonymousMode: true,
|
|
AutoCreateUsers: true,
|
|
TLSConfig: portainer.TLSConfiguration{},
|
|
SearchSettings: []portainer.LDAPSearchSettings{
|
|
{},
|
|
},
|
|
GroupSearchSettings: []portainer.LDAPGroupSearchSettings{
|
|
{},
|
|
},
|
|
},
|
|
OAuthSettings: portainer.OAuthSettings{
|
|
SSO: true,
|
|
},
|
|
SnapshotInterval: portainer.DefaultSnapshotInterval,
|
|
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
|
TemplatesURL: "",
|
|
HelmRepositoryURL: portainer.DefaultHelmRepositoryURL,
|
|
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
|
KubeconfigExpiry: portainer.DefaultKubeconfigExpiry,
|
|
KubectlShellImage: *store.flags.KubectlShellImage,
|
|
|
|
IsDockerDesktopExtension: isDDExtention,
|
|
EnforceEdgeID: true,
|
|
}
|
|
|
|
defaultSettings.ContainerAutomation.AutoHeal.Enabled = false
|
|
defaultSettings.ContainerAutomation.AutoHeal.CheckInterval = "30s"
|
|
defaultSettings.ContainerAutomation.AutoHeal.Scope = "labeled"
|
|
|
|
defaultSettings.ContainerAutomation.AutoUpdate.Enabled = false
|
|
defaultSettings.ContainerAutomation.AutoUpdate.PollInterval = "6h"
|
|
defaultSettings.ContainerAutomation.AutoUpdate.Scope = "labeled"
|
|
defaultSettings.ContainerAutomation.AutoUpdate.Cleanup = false
|
|
defaultSettings.ContainerAutomation.AutoUpdate.RollbackOnFailure = false
|
|
defaultSettings.ContainerAutomation.AutoUpdate.RollbackTimeout = "120s"
|
|
|
|
// The shared automation notification webhook is opt-in: empty by default.
|
|
defaultSettings.ContainerAutomation.Notification.WebhookURL = ""
|
|
|
|
return store.SettingsService.UpdateSettings(defaultSettings)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if settings.UserSessionTimeout == "" {
|
|
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
|
return store.Settings().UpdateSettings(settings)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *Store) checkOrCreateDefaultSSLSettings() error {
|
|
_, err := store.SSLSettings().Settings()
|
|
if store.IsErrObjectNotFound(err) {
|
|
defaultSSLSettings := &portainer.SSLSettings{
|
|
HTTPEnabled: true,
|
|
}
|
|
|
|
return store.SSLSettings().UpdateSettings(defaultSSLSettings)
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (store *Store) checkOrCreateDefaultData() error {
|
|
groups, err := store.EndpointGroupService.ReadAll()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(groups) == 0 {
|
|
unassignedGroup := &portainer.EndpointGroup{
|
|
Name: "Unassigned",
|
|
Description: "Unassigned environments",
|
|
Labels: []portainer.Pair{},
|
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
|
TagIDs: []portainer.TagID{},
|
|
}
|
|
|
|
err = store.EndpointGroupService.Create(unassignedGroup)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|