diff --git a/cmd/ansSendEvent.go b/cmd/ansSendEvent.go new file mode 100644 index 000000000..9376675f0 --- /dev/null +++ b/cmd/ansSendEvent.go @@ -0,0 +1,57 @@ +package cmd + +import ( + "encoding/json" + "github.com/SAP/jenkins-library/pkg/ans" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/telemetry" + "time" +) + +func ansSendEvent(config ansSendEventOptions, telemetryData *telemetry.CustomData) { + err := runAnsSendEvent(&config, &ans.ANS{}) + if err != nil { + log.Entry().WithError(err).Fatal("step execution failed") + } +} + +func runAnsSendEvent(config *ansSendEventOptions, c ans.Client) error { + ansServiceKey, err := ans.UnmarshallServiceKeyJSON(config.AnsServiceKey) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + c.SetServiceKey(ansServiceKey) + + event := ans.Event{ + EventType: config.EventType, + Severity: config.Severity, + Category: config.Category, + Subject: config.Subject, + Body: config.Body, + Priority: config.Priority, + Tags: config.Tags, + Resource: &ans.Resource{ + ResourceName: config.ResourceName, + ResourceType: config.ResourceType, + ResourceInstance: config.ResourceInstance, + Tags: config.ResourceTags, + }, + } + + if GeneralConfig.Verbose { + eventJson, _ := json.MarshalIndent(event, "", " ") + log.Entry().Infof("Event details: %s", eventJson) + } + + if err = event.Validate(); err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + // We set the time + event.EventTimestamp = time.Now().Unix() + if err = c.Send(event); err != nil { + log.SetErrorCategory(log.ErrorService) + } + return err +} diff --git a/cmd/ansSendEvent_generated.go b/cmd/ansSendEvent_generated.go new file mode 100644 index 000000000..903edd35b --- /dev/null +++ b/cmd/ansSendEvent_generated.go @@ -0,0 +1,269 @@ +// Code generated by piper's step-generator. DO NOT EDIT. + +package cmd + +import ( + "fmt" + "os" + "time" + + "github.com/SAP/jenkins-library/pkg/config" + "github.com/SAP/jenkins-library/pkg/log" + "github.com/SAP/jenkins-library/pkg/splunk" + "github.com/SAP/jenkins-library/pkg/telemetry" + "github.com/SAP/jenkins-library/pkg/validation" + "github.com/spf13/cobra" +) + +type ansSendEventOptions struct { + AnsServiceKey string `json:"ansServiceKey,omitempty"` + EventType string `json:"eventType,omitempty"` + Severity string `json:"severity,omitempty" validate:"possible-values=INFO NOTICE WARNING ERROR FATAL"` + Category string `json:"category,omitempty" validate:"possible-values=NOTIFICATION ALERT EXCEPTION"` + Subject string `json:"subject,omitempty"` + Body string `json:"body,omitempty"` + Priority int `json:"priority,omitempty"` + Tags map[string]interface{} `json:"tags,omitempty"` + ResourceName string `json:"resourceName,omitempty"` + ResourceType string `json:"resourceType,omitempty"` + ResourceInstance string `json:"resourceInstance,omitempty"` + ResourceTags map[string]interface{} `json:"resourceTags,omitempty"` +} + +// AnsSendEventCommand Send Event to the SAP Alert Notification Service +func AnsSendEventCommand() *cobra.Command { + const STEP_NAME = "ansSendEvent" + + metadata := ansSendEventMetadata() + var stepConfig ansSendEventOptions + var startTime time.Time + var logCollector *log.CollectorHook + var splunkClient *splunk.Splunk + telemetryClient := &telemetry.Telemetry{} + + var createAnsSendEventCmd = &cobra.Command{ + Use: STEP_NAME, + Short: "Send Event to the SAP Alert Notification Service", + Long: `With this step one can send an Event to the SAP Alert Notification Service.`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + startTime = time.Now() + log.SetStepName(STEP_NAME) + log.SetVerbose(GeneralConfig.Verbose) + + GeneralConfig.GitHubAccessTokens = ResolveAccessTokens(GeneralConfig.GitHubTokens) + + path, _ := os.Getwd() + fatalHook := &log.FatalHook{CorrelationID: GeneralConfig.CorrelationID, Path: path} + log.RegisterHook(fatalHook) + + err := PrepareConfig(cmd, &metadata, STEP_NAME, &stepConfig, config.OpenPiperFile) + if err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + log.RegisterSecret(stepConfig.AnsServiceKey) + + if len(GeneralConfig.HookConfig.SentryConfig.Dsn) > 0 { + sentryHook := log.NewSentryHook(GeneralConfig.HookConfig.SentryConfig.Dsn, GeneralConfig.CorrelationID) + log.RegisterHook(&sentryHook) + } + + if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { + splunkClient = &splunk.Splunk{} + logCollector = &log.CollectorHook{CorrelationID: GeneralConfig.CorrelationID} + log.RegisterHook(logCollector) + } + + validation, err := validation.New(validation.WithJSONNamesForStructFields(), validation.WithPredefinedErrorMessages()) + if err != nil { + return err + } + if err = validation.ValidateStruct(stepConfig); err != nil { + log.SetErrorCategory(log.ErrorConfiguration) + return err + } + + return nil + }, + Run: func(_ *cobra.Command, _ []string) { + stepTelemetryData := telemetry.CustomData{} + stepTelemetryData.ErrorCode = "1" + handler := func() { + config.RemoveVaultSecretFiles() + stepTelemetryData.Duration = fmt.Sprintf("%v", time.Since(startTime).Milliseconds()) + stepTelemetryData.ErrorCategory = log.GetErrorCategory().String() + stepTelemetryData.PiperCommitHash = GitCommit + telemetryClient.SetData(&stepTelemetryData) + telemetryClient.Send() + if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { + splunkClient.Send(telemetryClient.GetData(), logCollector) + } + } + log.DeferExitHandler(handler) + defer handler() + telemetryClient.Initialize(GeneralConfig.NoTelemetry, STEP_NAME) + if len(GeneralConfig.HookConfig.SplunkConfig.Dsn) > 0 { + splunkClient.Initialize(GeneralConfig.CorrelationID, + GeneralConfig.HookConfig.SplunkConfig.Dsn, + GeneralConfig.HookConfig.SplunkConfig.Token, + GeneralConfig.HookConfig.SplunkConfig.Index, + GeneralConfig.HookConfig.SplunkConfig.SendLogs) + } + ansSendEvent(stepConfig, &stepTelemetryData) + stepTelemetryData.ErrorCode = "0" + log.Entry().Info("SUCCESS") + }, + } + + addAnsSendEventFlags(createAnsSendEventCmd, &stepConfig) + return createAnsSendEventCmd +} + +func addAnsSendEventFlags(cmd *cobra.Command, stepConfig *ansSendEventOptions) { + cmd.Flags().StringVar(&stepConfig.AnsServiceKey, "ansServiceKey", os.Getenv("PIPER_ansServiceKey"), "Service key JSON string to access the SAP Alert Notification Service") + cmd.Flags().StringVar(&stepConfig.EventType, "eventType", `Piper`, "Type of the event") + cmd.Flags().StringVar(&stepConfig.Severity, "severity", `INFO`, "Event severity") + cmd.Flags().StringVar(&stepConfig.Category, "category", `NOTIFICATION`, "Event category") + cmd.Flags().StringVar(&stepConfig.Subject, "subject", `ansSendEvent`, "Short description of the event") + cmd.Flags().StringVar(&stepConfig.Body, "body", `Call from Piper step ansSendEvent`, "Detailed description of the event") + cmd.Flags().IntVar(&stepConfig.Priority, "priority", 0, "Event priority in the range of 1 to 1000") + + cmd.Flags().StringVar(&stepConfig.ResourceName, "resourceName", `Pipeline`, "Unique resource name") + cmd.Flags().StringVar(&stepConfig.ResourceType, "resourceType", `Pipeline`, "Resource type identifier") + cmd.Flags().StringVar(&stepConfig.ResourceInstance, "resourceInstance", os.Getenv("PIPER_resourceInstance"), "Optional resource instance identifier") + + cmd.MarkFlagRequired("ansServiceKey") +} + +// retrieve step metadata +func ansSendEventMetadata() config.StepData { + var theMetaData = config.StepData{ + Metadata: config.StepMetadata{ + Name: "ansSendEvent", + Aliases: []config.Alias{}, + Description: "Send Event to the SAP Alert Notification Service", + }, + Spec: config.StepSpec{ + Inputs: config.StepInputs{ + Secrets: []config.StepSecrets{ + {Name: "ansServiceKeyCredentialsId", Description: "Jenkins secret text credential ID containing the service key to access the SAP Alert Notification Service", Type: "jenkins"}, + }, + Parameters: []config.StepParameters{ + { + Name: "ansServiceKey", + ResourceRef: []config.ResourceReference{ + { + Name: "ansServiceKeyCredentialsId", + Param: "ansServiceKey", + Type: "secret", + }, + }, + Scope: []string{"PARAMETERS"}, + Type: "string", + Mandatory: true, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_ansServiceKey"), + }, + { + Name: "eventType", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `Piper`, + }, + { + Name: "severity", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `INFO`, + }, + { + Name: "category", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `NOTIFICATION`, + }, + { + Name: "subject", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `ansSendEvent`, + }, + { + Name: "body", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `Call from Piper step ansSendEvent`, + }, + { + Name: "priority", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "int", + Mandatory: false, + Aliases: []config.Alias{}, + Default: 0, + }, + { + Name: "tags", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "map[string]interface{}", + Mandatory: false, + Aliases: []config.Alias{}, + }, + { + Name: "resourceName", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `Pipeline`, + }, + { + Name: "resourceType", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: `Pipeline`, + }, + { + Name: "resourceInstance", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "string", + Mandatory: false, + Aliases: []config.Alias{}, + Default: os.Getenv("PIPER_resourceInstance"), + }, + { + Name: "resourceTags", + ResourceRef: []config.ResourceReference{}, + Scope: []string{"PARAMETERS", "STAGES", "STEPS"}, + Type: "map[string]interface{}", + Mandatory: false, + Aliases: []config.Alias{}, + }, + }, + }, + }, + } + return theMetaData +} diff --git a/cmd/ansSendEvent_generated_test.go b/cmd/ansSendEvent_generated_test.go new file mode 100644 index 000000000..4c3cd8209 --- /dev/null +++ b/cmd/ansSendEvent_generated_test.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAnsSendEventCommand(t *testing.T) { + t.Parallel() + + testCmd := AnsSendEventCommand() + + // only high level testing performed - details are tested in step generation procedure + assert.Equal(t, "ansSendEvent", testCmd.Use, "command name incorrect") + +} diff --git a/cmd/ansSendEvent_test.go b/cmd/ansSendEvent_test.go new file mode 100644 index 000000000..e41f43fb5 --- /dev/null +++ b/cmd/ansSendEvent_test.go @@ -0,0 +1,129 @@ +package cmd + +import ( + "fmt" + "github.com/SAP/jenkins-library/pkg/ans" + "github.com/SAP/jenkins-library/pkg/xsuaa" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +const testTimestamp = 1651585103 + +func TestRunAnsSendEvent(t *testing.T) { + tests := []struct { + name string + config ansSendEventOptions + ansMock ansMock + wantErrMsg string + }{ + { + name: "overwriting EventType", + config: defaultEventOptions(), + }, + { + name: "bad service key", + config: ansSendEventOptions{AnsServiceKey: `{"forgot": "closing", "bracket": "json"`}, + wantErrMsg: `error unmarshalling ANS serviceKey: unexpected end of JSON input`, + }, + { + name: "invalid event json", + config: ansSendEventOptions{AnsServiceKey: goodServiceKey, Severity: "WRONG_SEVERITY"}, + wantErrMsg: `Severity must be one of [INFO NOTICE WARNING ERROR FATAL]: event JSON failed the validation`, + }, + { + name: "fail to send", + config: defaultEventOptions(), + ansMock: ansMock{failToSend: true}, + wantErrMsg: `failed to send`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := runAnsSendEvent(&tt.config, &tt.ansMock); tt.wantErrMsg != "" { + assert.EqualError(t, err, tt.wantErrMsg) + } else { + require.NoError(t, err) + assert.Equal(t, "https://my.test.backend", tt.ansMock.testANS.URL) + assert.Equal(t, defaultXsuaa(), tt.ansMock.testANS.XSUAA) + assert.Equal(t, defaultEvent(), tt.ansMock.testEvent) + } + + }) + } +} + +func defaultEventOptions() ansSendEventOptions { + return ansSendEventOptions{ + AnsServiceKey: goodServiceKey, + EventType: "myEvent", + Severity: "INFO", + Category: "NOTIFICATION", + Subject: "testStep", + Body: "Call from Piper step: testStep", + Priority: 123, + Tags: map[string]interface{}{"myNumber": 456}, + ResourceName: "myResourceName", + ResourceType: "myResourceType", + ResourceInstance: "myResourceInstance", + ResourceTags: map[string]interface{}{"myBoolean": true}, + } +} + +func defaultEvent() ans.Event { + return ans.Event{ + EventType: "myEvent", + EventTimestamp: testTimestamp, + Severity: "INFO", + Category: "NOTIFICATION", + Subject: "testStep", + Body: "Call from Piper step: testStep", + Priority: 123, + Tags: map[string]interface{}{"myNumber": 456}, + Resource: &ans.Resource{ + ResourceName: "myResourceName", + ResourceType: "myResourceType", + ResourceInstance: "myResourceInstance", + Tags: map[string]interface{}{"myBoolean": true}, + }, + } +} + +func defaultXsuaa() xsuaa.XSUAA { + return xsuaa.XSUAA{ + OAuthURL: "https://my.test.oauth.provider", + ClientID: "myTestClientID", + ClientSecret: "super secret", + } +} + +const goodServiceKey = `{ + "url": "https://my.test.backend", + "client_id": "myTestClientID", + "client_secret": "super secret", + "oauth_url": "https://my.test.oauth.provider" + }` + +type ansMock struct { + testANS ans.ANS + testEvent ans.Event + failToSend bool +} + +func (am *ansMock) Send(event ans.Event) error { + if am.failToSend { + return fmt.Errorf("failed to send") + } + event.EventTimestamp = testTimestamp + am.testEvent = event + return nil +} + +func (am ansMock) CheckCorrectSetup() error { + return fmt.Errorf("not implemented") +} + +func (am *ansMock) SetServiceKey(serviceKey ans.ServiceKey) { + am.testANS.SetServiceKey(serviceKey) +} diff --git a/cmd/metadata_generated.go b/cmd/metadata_generated.go index eef833ef9..c8c577d42 100644 --- a/cmd/metadata_generated.go +++ b/cmd/metadata_generated.go @@ -25,6 +25,7 @@ func GetAllStepMetadata() map[string]config.StepData { "abapEnvironmentPushATCSystemConfig": abapEnvironmentPushATCSystemConfigMetadata(), "abapEnvironmentRunATCCheck": abapEnvironmentRunATCCheckMetadata(), "abapEnvironmentRunAUnitTest": abapEnvironmentRunAUnitTestMetadata(), + "ansSendEvent": ansSendEventMetadata(), "apiKeyValueMapDownload": apiKeyValueMapDownloadMetadata(), "apiKeyValueMapUpload": apiKeyValueMapUploadMetadata(), "apiProviderDownload": apiProviderDownloadMetadata(), diff --git a/cmd/piper.go b/cmd/piper.go index 2eb75f1da..90f2cd301 100644 --- a/cmd/piper.go +++ b/cmd/piper.go @@ -188,6 +188,7 @@ func Execute() { rootCmd.AddCommand(AzureBlobUploadCommand()) rootCmd.AddCommand(AwsS3UploadCommand()) rootCmd.AddCommand(ApiProxyListCommand()) + rootCmd.AddCommand(AnsSendEventCommand()) addRootFlags(rootCmd) diff --git a/resources/metadata/ansSendEvent.yaml b/resources/metadata/ansSendEvent.yaml new file mode 100644 index 000000000..fbb176302 --- /dev/null +++ b/resources/metadata/ansSendEvent.yaml @@ -0,0 +1,118 @@ +metadata: + name: ansSendEvent + description: Send Event to the SAP Alert Notification Service + longDescription: | + With this step one can send an Event to the SAP Alert Notification Service. + +spec: + inputs: + secrets: + - name: ansServiceKeyCredentialsId + description: Jenkins secret text credential ID containing the service key to access the SAP Alert Notification Service + type: jenkins + params: + - name: ansServiceKey + type: string + description: Service key JSON string to access the SAP Alert Notification Service + scope: + - PARAMETERS + mandatory: true + secret: true + resourceRef: + - name: ansServiceKeyCredentialsId + type: secret + param: ansServiceKey + - name: eventType + type: string + description: Type of the event + default: "Piper" + scope: + - PARAMETERS + - STAGES + - STEPS + - name: severity + type: string + description: Event severity + default: "INFO" + scope: + - PARAMETERS + - STAGES + - STEPS + possibleValues: + - INFO + - NOTICE + - WARNING + - ERROR + - FATAL + - name: category + type: string + description: Event category + default: "NOTIFICATION" + scope: + - PARAMETERS + - STAGES + - STEPS + possibleValues: + - NOTIFICATION + - ALERT + - EXCEPTION + - name: subject + type: string + description: Short description of the event + default: "ansSendEvent" + scope: + - PARAMETERS + - STAGES + - STEPS + - name: body + type: string + description: Detailed description of the event + default: "Call from Piper step ansSendEvent" + scope: + - PARAMETERS + - STAGES + - STEPS + - name: priority + type: int + description: Event priority in the range of 1 to 1000 + scope: + - PARAMETERS + - STAGES + - STEPS + - name: tags + type: "map[string]interface{}" + description: Optional key-value pairs describing the event in details + scope: + - PARAMETERS + - STAGES + - STEPS + - name: resourceName + type: string + description: Unique resource name + default: "Pipeline" + scope: + - PARAMETERS + - STAGES + - STEPS + - name: resourceType + type: string + description: Resource type identifier + default: "Pipeline" + scope: + - PARAMETERS + - STAGES + - STEPS + - name: resourceInstance + type: string + description: Optional resource instance identifier + scope: + - PARAMETERS + - STAGES + - STEPS + - name: resourceTags + type: "map[string]interface{}" + description: Optional key-value pairs describing the resource in details + scope: + - PARAMETERS + - STAGES + - STEPS diff --git a/test/groovy/AnsSendEventTest.groovy b/test/groovy/AnsSendEventTest.groovy new file mode 100644 index 000000000..39fdff002 --- /dev/null +++ b/test/groovy/AnsSendEventTest.groovy @@ -0,0 +1,56 @@ +import org.junit.Rule +import org.junit.Test +import org.junit.rules.RuleChain +import util.BasePiperTest +import util.JenkinsReadYamlRule +import util.JenkinsStepRule +import util.Rules + +import static org.hamcrest.Matchers.allOf +import static org.hamcrest.Matchers.hasEntry +import static org.hamcrest.Matchers.is +import static org.junit.Assert.assertThat + +public class AnsSendEventTest extends BasePiperTest { + + private JenkinsStepRule stepRule = new JenkinsStepRule(this) + private JenkinsReadYamlRule readYamlRule = new JenkinsReadYamlRule(this) + + @Rule + public RuleChain ruleChain = Rules + .getCommonRules(this) + .around(stepRule) + .around(readYamlRule) + + @Test + void testCallGoWrapper() { + + def calledWithParameters, + calledWithStepName, + calledWithMetadata + List calledWithCredentials + + helper.registerAllowedMethod( + 'piperExecuteBin', + [Map, String, String, List], + { + params, stepName, metaData, creds -> + calledWithParameters = params + calledWithStepName = stepName + calledWithMetadata = metaData + calledWithCredentials = creds + } + ) + + stepRule.step.ansSendEvent(script: nullScript, abc: 'ABC') + + assertThat(calledWithParameters.size(), is(2)) + assertThat(calledWithParameters.script, is(nullScript)) + assertThat(calledWithParameters.abc, is('ABC')) + + assertThat(calledWithStepName, is('ansSendEvent')) + assertThat(calledWithMetadata, is('metadata/ansSendEvent.yaml')) + assertThat(calledWithCredentials[0].size(), is(3)) + assertThat(calledWithCredentials[0], allOf(hasEntry('type', 'token'), hasEntry('id', 'ansServiceKeyCredentialsId'), hasEntry('env', ['PIPER_ansServiceKey']))) + } +} diff --git a/test/groovy/CommonStepsTest.groovy b/test/groovy/CommonStepsTest.groovy index 919b6375c..4ffea68ae 100644 --- a/test/groovy/CommonStepsTest.groovy +++ b/test/groovy/CommonStepsTest.groovy @@ -220,7 +220,8 @@ public class CommonStepsTest extends BasePiperTest{ 'awsS3Upload', 'apiProxyList', //implementing new golang pattern without fields 'azureBlobUpload', - 'awsS3Upload' + 'awsS3Upload', + 'ansSendEvent' ] @Test diff --git a/vars/ansSendEvent.groovy b/vars/ansSendEvent.groovy new file mode 100644 index 000000000..243f43bd8 --- /dev/null +++ b/vars/ansSendEvent.groovy @@ -0,0 +1,11 @@ +import groovy.transform.Field + +@Field String STEP_NAME = getClass().getName() +@Field String METADATA_FILE = 'metadata/ansSendEvent.yaml' + +void call(Map parameters = [:]) { + List credentials = [ + [type: 'token', id: 'ansServiceKeyCredentialsId', env: ['PIPER_ansServiceKey']] + ] + piperExecuteBin(parameters, STEP_NAME, METADATA_FILE, credentials) +}