284 строки
9.5 KiB
Go
284 строки
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
|
|
"github.com/Azure/applicationhealth-extension-linux/internal/telemetry"
|
|
"github.com/Azure/azure-docker-extension/pkg/vmextension"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
var (
|
|
errTcpMustNotIncludeRequestPath = errors.New("'requestPath' cannot be specified when using 'tcp' protocol")
|
|
errTcpConfigurationMustIncludePort = errors.New("'port' must be specified when using 'tcp' protocol")
|
|
errProbeSettleTimeExceedsThreshold = errors.New("Probe settle time (intervalInSeconds * numberOfProbes) cannot exceed 240 seconds")
|
|
defaultIntervalInSeconds = 5
|
|
defaultNumberOfProbes = 1
|
|
maximumProbeSettleTime = 240
|
|
)
|
|
|
|
// handlerSettings holds the configuration of the extension handler.
|
|
type handlerSettings struct {
|
|
publicSettings
|
|
protectedSettings
|
|
}
|
|
|
|
func (s handlerSettings) String() string {
|
|
settings, _ := json.MarshalIndent(s, "", "\t")
|
|
return string(settings)
|
|
}
|
|
|
|
func (s *handlerSettings) protocol() string {
|
|
return s.publicSettings.Protocol
|
|
}
|
|
|
|
func (s *handlerSettings) requestPath() string {
|
|
return s.publicSettings.RequestPath
|
|
}
|
|
|
|
func (s *handlerSettings) port() int {
|
|
return s.publicSettings.Port
|
|
}
|
|
|
|
func (s *handlerSettings) intervalInSeconds() int {
|
|
var intervalInSeconds = s.publicSettings.IntervalInSeconds
|
|
if intervalInSeconds == 0 {
|
|
return defaultIntervalInSeconds
|
|
} else {
|
|
return intervalInSeconds
|
|
}
|
|
}
|
|
|
|
func (s *handlerSettings) numberOfProbes() int {
|
|
var numberOfProbes = s.publicSettings.NumberOfProbes
|
|
if numberOfProbes == 0 {
|
|
return defaultNumberOfProbes
|
|
} else {
|
|
return numberOfProbes
|
|
}
|
|
}
|
|
|
|
func (s *handlerSettings) gracePeriod() int {
|
|
var gracePeriod = s.publicSettings.GracePeriod
|
|
if gracePeriod == 0 {
|
|
return s.intervalInSeconds() * s.numberOfProbes()
|
|
} else {
|
|
return gracePeriod
|
|
}
|
|
}
|
|
|
|
func (s *handlerSettings) vmWatchSettings() *vmWatchSettings {
|
|
return s.publicSettings.VMWatchSettings
|
|
}
|
|
|
|
// validate makes logical validation on the handlerSettings which already passed
|
|
// the schema validation.
|
|
func (h handlerSettings) validate() error {
|
|
if h.protocol() == "tcp" && h.port() == 0 {
|
|
return errTcpConfigurationMustIncludePort
|
|
}
|
|
|
|
if h.protocol() == "tcp" && h.requestPath() != "" {
|
|
return errTcpMustNotIncludeRequestPath
|
|
}
|
|
|
|
probeSettlingTime := h.intervalInSeconds() * h.numberOfProbes()
|
|
if probeSettlingTime > maximumProbeSettleTime {
|
|
return errProbeSettleTimeExceedsThreshold
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type vmWatchSignalFilters struct {
|
|
EnabledTags []string `json:"enabledTags,array"`
|
|
DisabledTags []string `json:"disabledTags,array"`
|
|
EnabledOptionalSignals []string `json:"enabledOptionalSignals,array"`
|
|
DisabledSignals []string `json:"disabledSignals,array"`
|
|
}
|
|
|
|
type vmWatchSettings struct {
|
|
Enabled bool `json:"enabled,boolean"`
|
|
MemoryLimitInBytes int64 `json:"memoryLimitInBytes,int64"`
|
|
MaxCpuPercentage int64 `json:"maxCpuPercentage,int64"`
|
|
SignalFilters *vmWatchSignalFilters `json:"signalFilters"`
|
|
ParameterOverrides map[string]interface{} `json:"parameterOverrides,object"`
|
|
EnvironmentAttributes map[string]interface{} `json:"environmentAttributes,object"`
|
|
GlobalConfigUrl string `json:"globalConfigUrl"`
|
|
DisableConfigReader bool `json:"disableConfigReader,boolean"`
|
|
}
|
|
|
|
func (v *vmWatchSettings) String() string {
|
|
setting, _ := json.MarshalIndent(v, "", "\t")
|
|
return string(setting)
|
|
}
|
|
|
|
// Case insensitive retrieval of VMWatchCohortId from EnvironmentAttributes
|
|
// We serialize and deserialize EnvironmentAttributes to ensure case insensitive get for VMWatchCohortId
|
|
func (v *vmWatchSettings) TryGetVMWatchCohortId() (vmWatchCohortId string, err error) {
|
|
if v.EnvironmentAttributes == nil {
|
|
return "", nil
|
|
}
|
|
|
|
var bytes []byte
|
|
if bytes, err = json.Marshal(v.EnvironmentAttributes); err != nil {
|
|
return "", errors.Wrap(err, "failed to marshal environment attributes")
|
|
}
|
|
|
|
var envAttributes environmentAttributes
|
|
if err := json.Unmarshal(bytes, &envAttributes); err != nil {
|
|
return "", errors.Wrap(err, "failed to unmarshal environment attributes")
|
|
}
|
|
|
|
return envAttributes.VMWatchCohortId, nil
|
|
}
|
|
|
|
// This struct is only for retrieving VMWatchCohortId in a case insensitive way.
|
|
// The full EnvironmentAttribute dictionary is passed to VMWatch through VMWatchSettings.
|
|
type environmentAttributes struct {
|
|
VMWatchCohortId string `json:"vmWatchCohortId"`
|
|
}
|
|
|
|
// publicSettings is the type deserialized from public configuration section of
|
|
// the extension handler. This should be in sync with publicSettingsSchema.
|
|
type publicSettings struct {
|
|
Protocol string `json:"protocol"`
|
|
Port int `json:"port,int"`
|
|
RequestPath string `json:"requestPath"`
|
|
IntervalInSeconds int `json:"intervalInSeconds,int"`
|
|
NumberOfProbes int `json:"numberOfProbes,int"`
|
|
GracePeriod int `json:"gracePeriod,int"`
|
|
VMWatchSettings *vmWatchSettings `json:"vmWatchSettings"`
|
|
}
|
|
|
|
// protectedSettings is the type decoded and deserialized from protected
|
|
// configuration section. This should be in sync with protectedSettingsSchema.
|
|
type protectedSettings struct {
|
|
}
|
|
|
|
// parseAndValidateSettings reads configuration from configFolder, decrypts it,
|
|
// runs JSON-schema and logical validation on it and returns it back.
|
|
func parseAndValidateSettings(lg *slog.Logger, configFolder string) (h handlerSettings, _ error) {
|
|
telemetry.SendEvent(telemetry.InfoEvent, telemetry.MainTask, "Reading configuration")
|
|
pubJSON, protJSON, err := readSettings(configFolder)
|
|
if err != nil {
|
|
return h, err
|
|
}
|
|
|
|
telemetry.SendEvent(telemetry.InfoEvent, telemetry.MainTask, "validating json schema")
|
|
if err := validateSettingsSchema(pubJSON, protJSON); err != nil {
|
|
return h, errors.Wrap(err, "json validation error")
|
|
}
|
|
|
|
telemetry.SendEvent(telemetry.InfoEvent, telemetry.MainTask, "json schema valid")
|
|
telemetry.SendEvent(telemetry.InfoEvent, telemetry.MainTask, "parsing configuration json")
|
|
if err := vmextension.UnmarshalHandlerSettings(pubJSON, protJSON, &h.publicSettings, &h.protectedSettings); err != nil {
|
|
return h, errors.Wrap(err, "json parsing error")
|
|
}
|
|
telemetry.SendEvent(telemetry.InfoEvent, telemetry.MainTask, "parsed configuration json")
|
|
|
|
telemetry.SendEvent(telemetry.InfoEvent, telemetry.MainTask, "validating configuration logically")
|
|
if err := h.validate(); err != nil {
|
|
return h, errors.Wrap(err, "invalid configuration")
|
|
}
|
|
telemetry.SendEvent(telemetry.InfoEvent, telemetry.MainTask, "validated configuration")
|
|
return h, nil
|
|
}
|
|
|
|
// readSettings uses specified configFolder (comes from HandlerEnvironment) to
|
|
// decrypt and parse the public/protected settings of the extension handler into
|
|
// JSON objects.
|
|
func readSettings(configFolder string) (pubSettingsJSON, protSettingsJSON map[string]interface{}, err error) {
|
|
pubSettingsJSON, protSettingsJSON, err = vmextension.ReadSettings(configFolder)
|
|
err = errors.Wrapf(err, "error reading extension configuration")
|
|
return
|
|
}
|
|
|
|
// validateSettings takes publicSettings and protectedSettings as JSON objects
|
|
// and runs JSON schema validation on them.
|
|
func validateSettingsSchema(pubSettingsJSON, protSettingsJSON map[string]interface{}) error {
|
|
pubJSON, err := toJSON(pubSettingsJSON)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to unmarshal public settings into json")
|
|
}
|
|
protJSON, err := toJSON(protSettingsJSON)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to unmarshal protected settings into json")
|
|
}
|
|
|
|
if err := validatePublicSettings(pubJSON); err != nil {
|
|
return err
|
|
}
|
|
if err := validateProtectedSettings(protJSON); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// toJSON converts given in-memory JSON object representation into a JSON object string.
|
|
func toJSON(o map[string]interface{}) (string, error) {
|
|
if o == nil { // instead of JSON 'null' assume empty object '{}'
|
|
return "{}", nil
|
|
}
|
|
b, err := json.Marshal(o)
|
|
return string(b), errors.Wrap(err, "failed to marshal into json")
|
|
}
|
|
|
|
type ExtensionManifest struct {
|
|
ProviderNameSpace string `xml:"ProviderNameSpace"`
|
|
Type string `xml:"Type"`
|
|
Version string `xml:"Version"`
|
|
Label string `xml:"Label"`
|
|
HostingResources string `xml:"HostingResources"`
|
|
MediaLink string `xml:"MediaLink"`
|
|
Description string `xml:"Description"`
|
|
IsInternalExtension bool `xml:"IsInternalExtension"`
|
|
IsJsonExtension bool `xml:"IsJsonExtension"`
|
|
SupportedOS string `xml:"SupportedOS"`
|
|
CompanyName string `xml:"CompanyName"`
|
|
}
|
|
|
|
func GetExtensionManifest(filepath string) (ExtensionManifest, error) {
|
|
file, err := os.Open(filepath)
|
|
if err != nil {
|
|
return ExtensionManifest{}, err
|
|
}
|
|
defer file.Close()
|
|
|
|
decoder := xml.NewDecoder(file)
|
|
var manifest ExtensionManifest
|
|
err = decoder.Decode(&manifest)
|
|
|
|
if err != nil {
|
|
return ExtensionManifest{}, err
|
|
}
|
|
return manifest, nil
|
|
}
|
|
|
|
// Get Extension Version set at build time or from manifest file.
|
|
func GetExtensionManifestVersion() (string, error) {
|
|
// First attempting to read the version set during build time.
|
|
v := GetExtensionVersion()
|
|
if v != "" {
|
|
return v, nil
|
|
}
|
|
|
|
// If the version is not set during build time, then reading it from the manifest file as fallback.
|
|
processDirectory, err := GetProcessDirectory()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
processDirectory = filepath.Dir(processDirectory)
|
|
fp := filepath.Join(processDirectory, ExtensionManifestFileName)
|
|
|
|
manifest, err := GetExtensionManifest(fp)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return manifest.Version, nil
|
|
}
|