From e126f63965e3be72c3f2e0ad48bc0c28c663a820 Mon Sep 17 00:00:00 2001 From: andres-portainer <91705312+andres-portainer@users.noreply.github.com> Date: Thu, 11 Nov 2021 15:49:50 -0300 Subject: [PATCH] feat(openamt): add feature flag for OpenAMT [INT-5] (#6049) feat(openamt): add feature flag for OpenAMT [INT-5] --- api/cli/cli.go | 1 + api/cli/pairlist.go | 5 +- api/cli/pairlistbool.go | 45 ++++++++++++++++++ api/cmd/portainer/main.go | 50 ++++++++++++++++++++ api/http/handler/settings/settings_public.go | 3 ++ api/portainer.go | 17 +++++++ app/portainer/models/settings.js | 2 + 7 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 api/cli/pairlistbool.go diff --git a/api/cli/cli.go b/api/cli/cli.go index 4518df71f..cc1988860 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -36,6 +36,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) { Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(), Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(), EndpointURL: kingpin.Flag("host", "Environment URL").Short('H').String(), + FeatureFlags: boolPairs(kingpin.Flag("feat", "List of feature flags").Hidden()), EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(), NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(), TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(), diff --git a/api/cli/pairlist.go b/api/cli/pairlist.go index 1eb6d10df..2d42d450f 100644 --- a/api/cli/pairlist.go +++ b/api/cli/pairlist.go @@ -1,11 +1,12 @@ package cli import ( - "github.com/portainer/portainer/api" + portainer "github.com/portainer/portainer/api" "fmt" - "gopkg.in/alecthomas/kingpin.v2" "strings" + + "gopkg.in/alecthomas/kingpin.v2" ) type pairList []portainer.Pair diff --git a/api/cli/pairlistbool.go b/api/cli/pairlistbool.go new file mode 100644 index 000000000..e1411de6b --- /dev/null +++ b/api/cli/pairlistbool.go @@ -0,0 +1,45 @@ +package cli + +import ( + portainer "github.com/portainer/portainer/api" + + "strings" + + "gopkg.in/alecthomas/kingpin.v2" +) + +type pairListBool []portainer.Pair + +// Set implementation for a list of portainer.Pair +func (l *pairListBool) Set(value string) error { + p := new(portainer.Pair) + + // default to true. example setting=true is equivalent to setting + parts := strings.SplitN(value, "=", 2) + if len(parts) != 2 { + p.Name = parts[0] + p.Value = "true" + } else { + p.Name = parts[0] + p.Value = parts[1] + } + + *l = append(*l, *p) + return nil +} + +// String implementation for a list of pair +func (l *pairListBool) String() string { + return "" +} + +// IsCumulative implementation for a list of pair +func (l *pairListBool) IsCumulative() bool { + return true +} + +func boolPairs(s kingpin.Settings) (target *[]portainer.Pair) { + target = new([]portainer.Pair) + s.SetValue((*pairListBool)(target)) + return +} diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 2886468f8..52b4c2080 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -2,8 +2,10 @@ package main import ( "context" + "fmt" "log" "os" + "strconv" "strings" portainer "github.com/portainer/portainer/api" @@ -237,6 +239,49 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI return nil } +// enableFeaturesFromFlags turns on or off feature flags +// e.g. portainer --feat open-amt --feat fdo=true ... (defaults to true) +// note, settins persisted to the DB. To turn off --feat open-amt=false +func enableFeaturesFromFlags(dataStore portainer.DataStore, flags *portainer.CLIFlags) error { + settings, err := dataStore.Settings().Settings() + if err != nil { + return err + } + + if settings.FeatureFlagSettings == nil { + settings.FeatureFlagSettings = make(map[portainer.Feature]bool) + } + + // loop through feature flags to check if they are supported + for _, feat := range *flags.FeatureFlags { + var correspondingFeature *portainer.Feature + for i, supportedFeat := range portainer.SupportedFeatureFlags { + if strings.EqualFold(feat.Name, string(supportedFeat)) { + correspondingFeature = &portainer.SupportedFeatureFlags[i] + } + } + + if correspondingFeature == nil { + return fmt.Errorf("unknown feature flag '%s'", feat.Name) + } + + featureState, err := strconv.ParseBool(feat.Value) + if err != nil { + return fmt.Errorf("feature flag's '%s' value should be true or false", feat.Name) + } + + if featureState { + log.Printf("Feature %v : on", *correspondingFeature) + } else { + log.Printf("Feature %v : off", *correspondingFeature) + } + + settings.FeatureFlagSettings[*correspondingFeature] = featureState + } + + return dataStore.Settings().UpdateSettings(settings) +} + func loadAndParseKeyPair(fileService portainer.FileService, signatureService portainer.DigitalSignatureService) error { private, public, err := fileService.LoadKeyPair() if err != nil { @@ -492,6 +537,11 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server { } } + err = enableFeaturesFromFlags(dataStore, flags) + if err != nil { + log.Fatalf("failed enabling feature flag: %v", err) + } + err = edge.LoadEdgeJobs(dataStore, reverseTunnelService) if err != nil { log.Fatalf("failed loading edge jobs from database: %v", err) diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index dbe8c243e..3849dffc3 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -16,6 +16,8 @@ type publicSettingsResponse struct { AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod" example:"1"` // Whether edge compute features are enabled EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures" example:"true"` + // Supported feature flags + Features map[portainer.Feature]bool `json:"Features"` // The URL used for oauth login OAuthLoginURI string `json:"OAuthLoginURI" example:"https://gitlab.com/oauth"` // The URL used for oauth logout @@ -52,6 +54,7 @@ func generatePublicSettings(appSettings *portainer.Settings) *publicSettingsResp EnableEdgeComputeFeatures: appSettings.EnableEdgeComputeFeatures, EnableTelemetry: appSettings.EnableTelemetry, KubeconfigExpiry: appSettings.KubeconfigExpiry, + Features: appSettings.FeatureFlagSettings, } //if OAuth authentication is on, compose the related fields from application settings if publicSettings.AuthenticationMethod == portainer.AuthenticationOAuth { diff --git a/api/portainer.go b/api/portainer.go index 43703bc4c..64f74c2b0 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -50,6 +50,7 @@ type ( AdminPasswordFile *string Assets *string Data *string + FeatureFlags *[]Pair EnableEdgeComputeFeatures *bool EndpointURL *string Labels *[]Pair @@ -388,6 +389,9 @@ type ( // ExtensionID represents a extension identifier ExtensionID int + // Feature represents a feature that can be enabled or disabled via feature flags + Feature string + // GitlabRegistryData represents data required for gitlab registry to work GitlabRegistryData struct { ProjectID int `json:"ProjectId"` @@ -703,6 +707,7 @@ type ( AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod" example:"1"` LDAPSettings LDAPSettings `json:"LDAPSettings" example:""` OAuthSettings OAuthSettings `json:"OAuthSettings" example:""` + FeatureFlagSettings map[Feature]bool `json:"FeatureFlagSettings" example:""` // The interval in which environment(endpoint) snapshots are created SnapshotInterval string `json:"SnapshotInterval" example:"5m"` // URL to the templates that will be displayed in the UI when navigating to App Templates @@ -1514,6 +1519,18 @@ const ( WebSocketKeepAlive = 1 * time.Hour ) +// Supported feature flags +const ( + FeatOpenAMT Feature = "open-amt" + FeatFDO Feature = "fdo" +) + +// List of supported features +var SupportedFeatureFlags = []Feature{ + FeatOpenAMT, + FeatFDO, +} + const ( _ AuthenticationMethod = iota // AuthenticationInternal represents the internal authentication method (authentication against Portainer API) diff --git a/app/portainer/models/settings.js b/app/portainer/models/settings.js index 05bdc832b..06226b5bb 100644 --- a/app/portainer/models/settings.js +++ b/app/portainer/models/settings.js @@ -8,6 +8,7 @@ export function SettingsViewModel(data) { this.TemplatesURL = data.TemplatesURL; this.EdgeAgentCheckinInterval = data.EdgeAgentCheckinInterval; this.EnableEdgeComputeFeatures = data.EnableEdgeComputeFeatures; + this.FeatureFlagSettings = data.FeatureFlagSettings; this.UserSessionTimeout = data.UserSessionTimeout; this.EnableTelemetry = data.EnableTelemetry; this.KubeconfigExpiry = data.KubeconfigExpiry; @@ -17,6 +18,7 @@ export function SettingsViewModel(data) { export function PublicSettingsViewModel(settings) { this.AuthenticationMethod = settings.AuthenticationMethod; this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures; + this.FeatureFlagSettings = settings.FeatureFlagSettings; this.LogoURL = settings.LogoURL; this.OAuthLoginURI = settings.OAuthLoginURI; this.EnableTelemetry = settings.EnableTelemetry;