From 0874051eb4dd2f20e6c4b19e750d37931b426330 Mon Sep 17 00:00:00 2001 From: Arthur Pitman Date: Thu, 23 Jun 2022 14:52:47 +0200 Subject: [PATCH] feat: Add `v2.KeptnInterface` that adds `context.Context` support to `api.KeptnInterface` (#449) * Add `context.Context` to request methods Signed-off-by: Arthur Pitman * Add get method Signed-off-by: Arthur Pitman * Use get methods Signed-off-by: Arthur Pitman * Add context to `APIV1Interface` Signed-off-by: Arthur Pitman * Add context to `AuthV1Interface` Signed-off-by: Arthur Pitman * Add context to `EventsV1Interface` Signed-off-by: Arthur Pitman * Add context to `LogsV1Interface` Signed-off-by: Arthur Pitman * Add context to `ProjectsV1Interface` Signed-off-by: Arthur Pitman * Add initial context support to `ResourcesV1Interface` Signed-off-by: Arthur Pitman * Add context to `ResourcesV1Interface` Signed-off-by: Arthur Pitman * Add context to `SecretsV1Interface` Signed-off-by: Arthur Pitman * Add context to `SequencesV1Interface` Signed-off-by: Arthur Pitman * Add context to `ServicesV1Interface` Signed-off-by: Arthur Pitman * Add context to `ShipyardControlV1Interface` Signed-off-by: Arthur Pitman * Add context to `StagesV1Interface` Signed-off-by: Arthur Pitman * Add context to `UniformV1Interface` Signed-off-by: Arthur Pitman * Add context to `Keptn` and `KeptnBase` Signed-off-by: Arthur Pitman * Fix comments Signed-off-by: Arthur Pitman * Introduce APIV2Interface Signed-off-by: Arthur Pitman * Second option: APIV2Interface with `...WithContext` methods Signed-off-by: Arthur Pitman * Create `v2` as duplicate of `api` package Signed-off-by: Arthur Pitman * Fix `v2.AuthInterface` Signed-off-by: Arthur Pitman * Fix `v2.EventsInterface` Signed-off-by: Arthur Pitman * Fix `v2.LogsInterface` Signed-off-by: Arthur Pitman * Fix `v2.ProjectsInterface` Signed-off-by: Arthur Pitman * Fix `v2.ResourcesInterface` Signed-off-by: Arthur Pitman * Fix `v2.SecretsInterface` Signed-off-by: Arthur Pitman * Fix `v2.SequencesInterface` Signed-off-by: Arthur Pitman * Fix `v2.ServicesInterface` Signed-off-by: Arthur Pitman * Fix `v2.ShipyardControlInterface` Signed-off-by: Arthur Pitman * Fix `v2.StagesInterface` Signed-off-by: Arthur Pitman * Fix `v2.UniformInterface` Signed-off-by: Arthur Pitman * Fix `v2.Client` Signed-off-by: Arthur Pitman * APIHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * AuthHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * EventHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * LogHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * ProjectHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * SecretHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * SequenceControlHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * ServiceHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * ShipyardControllerHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * StageHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * UniformHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * Revert "Add context to `Keptn` and `KeptnBase`" Signed-off-by: Arthur Pitman * Remove redundant test Signed-off-by: Arthur Pitman * Revert `ResourcesV1Interface` Signed-off-by: Arthur Pitman * Rename and export implementation methods Signed-off-by: Arthur Pitman * Fix options for `v2.ResourceHandler` methods Signed-off-by: Arthur Pitman * ResourceHandler: v1 embeds v2 Signed-off-by: Arthur Pitman * Remove redundant test Signed-off-by: Arthur Pitman * Add construction functions Signed-off-by: Arthur Pitman * Harmonize construction functions Signed-off-by: Arthur Pitman * Remove deprecated functions from v2 Signed-off-by: Arthur Pitman * Add v2 construction functions Signed-off-by: Arthur Pitman * Polish v2 handlers Signed-off-by: Arthur Pitman * Add missing functions for APIHandler Signed-off-by: Arthur Pitman * added missing api handler in GetMetadata() Signed-off-by: Florian Bacher * added alias for v2.ResourceNotFoundError to stay backwards compatible Signed-off-by: Florian Bacher * fix: Make sure all api handlers are set when using v2 api handlers in v1 (#479) * debug output Signed-off-by: Florian Bacher * debug output Signed-off-by: Florian Bacher * ensure that uniform handler is set Signed-off-by: Florian Bacher * ensure that uniform handler is set Signed-off-by: Florian Bacher * ensure that all api handlers are set Signed-off-by: Florian Bacher Co-authored-by: Florian Bacher --- pkg/api/utils/apiServiceUtils.go | 163 ++++-- pkg/api/utils/apiUtils.go | 172 +++--- pkg/api/utils/authUtils.go | 65 ++- pkg/api/utils/client.go | 2 +- pkg/api/utils/eventUtils.go | 182 ++----- pkg/api/utils/logUtils.go | 165 ++---- pkg/api/utils/projectUtils.go | 186 +++---- pkg/api/utils/resourceUtils.go | 496 ++++++----------- pkg/api/utils/secretUtils.go | 126 ++--- pkg/api/utils/sequenceUtils.go | 83 +-- pkg/api/utils/serviceUtils.go | 169 ++---- pkg/api/utils/shipyardControllerUtils.go | 136 ++--- pkg/api/utils/stageUtils.go | 123 ++--- pkg/api/utils/uniformUtils.go | 148 ++--- pkg/api/utils/v2/apiServiceUtils.go | 340 ++++++++++++ pkg/api/utils/v2/apiServiceUtils_test.go | 27 + pkg/api/utils/v2/apiUtils.go | 226 ++++++++ pkg/api/utils/{ => v2}/apiUtils_test.go | 7 +- pkg/api/utils/v2/authUtils.go | 71 +++ pkg/api/utils/v2/client.go | 181 +++++++ pkg/api/utils/v2/client_test.go | 61 +++ pkg/api/utils/v2/errors.go | 19 + pkg/api/utils/v2/eventUtils.go | 192 +++++++ pkg/api/utils/v2/eventwatch.go | 137 +++++ pkg/api/utils/v2/eventwatch_test.go | 136 +++++ pkg/api/utils/v2/fake/log_handler_mock.go | 286 ++++++++++ pkg/api/utils/v2/fake/secret_handler_mock.go | 249 +++++++++ pkg/api/utils/v2/healthCheck.go | 48 ++ pkg/api/utils/v2/logUtils.go | 218 ++++++++ pkg/api/utils/{ => v2}/logUtils_test.go | 27 +- pkg/api/utils/v2/projectUtils.go | 179 +++++++ pkg/api/utils/v2/resourceUtils.go | 534 +++++++++++++++++++ pkg/api/utils/{ => v2}/resourceUtils_test.go | 9 +- pkg/api/utils/v2/secretUtils.go | 146 +++++ pkg/api/utils/v2/sequenceUtils.go | 147 +++++ pkg/api/utils/{ => v2}/sequenceUtils_test.go | 5 +- pkg/api/utils/v2/serviceUtils.go | 170 ++++++ pkg/api/utils/v2/shipyardControllerUtils.go | 135 +++++ pkg/api/utils/v2/sleep.go | 43 ++ pkg/api/utils/v2/sleep_test.go | 25 + pkg/api/utils/v2/stageUtils.go | 130 +++++ pkg/api/utils/v2/uniformUtils.go | 177 ++++++ pkg/lib/v0_2_0/keptn.go | 2 +- 43 files changed, 4738 insertions(+), 1405 deletions(-) create mode 100644 pkg/api/utils/v2/apiServiceUtils.go create mode 100644 pkg/api/utils/v2/apiServiceUtils_test.go create mode 100644 pkg/api/utils/v2/apiUtils.go rename pkg/api/utils/{ => v2}/apiUtils_test.go (94%) create mode 100644 pkg/api/utils/v2/authUtils.go create mode 100644 pkg/api/utils/v2/client.go create mode 100644 pkg/api/utils/v2/client_test.go create mode 100644 pkg/api/utils/v2/errors.go create mode 100644 pkg/api/utils/v2/eventUtils.go create mode 100644 pkg/api/utils/v2/eventwatch.go create mode 100644 pkg/api/utils/v2/eventwatch_test.go create mode 100644 pkg/api/utils/v2/fake/log_handler_mock.go create mode 100644 pkg/api/utils/v2/fake/secret_handler_mock.go create mode 100644 pkg/api/utils/v2/healthCheck.go create mode 100644 pkg/api/utils/v2/logUtils.go rename pkg/api/utils/{ => v2}/logUtils_test.go (91%) create mode 100644 pkg/api/utils/v2/projectUtils.go create mode 100644 pkg/api/utils/v2/resourceUtils.go rename pkg/api/utils/{ => v2}/resourceUtils_test.go (96%) create mode 100644 pkg/api/utils/v2/secretUtils.go create mode 100644 pkg/api/utils/v2/sequenceUtils.go rename pkg/api/utils/{ => v2}/sequenceUtils_test.go (92%) create mode 100644 pkg/api/utils/v2/serviceUtils.go create mode 100644 pkg/api/utils/v2/shipyardControllerUtils.go create mode 100644 pkg/api/utils/v2/sleep.go create mode 100644 pkg/api/utils/v2/sleep_test.go create mode 100644 pkg/api/utils/v2/stageUtils.go create mode 100644 pkg/api/utils/v2/uniformUtils.go diff --git a/pkg/api/utils/apiServiceUtils.go b/pkg/api/utils/apiServiceUtils.go index 92df36e4..4ee0edfb 100644 --- a/pkg/api/utils/apiServiceUtils.go +++ b/pkg/api/utils/apiServiceUtils.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "context" "crypto/tls" "fmt" "io/ioutil" @@ -61,11 +62,66 @@ func getClientTransport(rt http.RoundTripper) http.RoundTripper { return tr } return rt +} + +func getAndExpectOK(ctx context.Context, uri string, api APIService) ([]byte, *models.Error) { + body, statusCode, status, err := get(ctx, uri, api) + if err != nil { + return nil, err + } + if statusCode == 200 { + return body, nil + } + + if len(body) > 0 { + return nil, handleErrStatusCode(statusCode, body) + } + + return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", statusCode, status)) } -func putWithEventContext(uri string, data []byte, api APIService) (*models.EventContext, *models.Error) { - req, err := http.NewRequest("PUT", uri, bytes.NewBuffer(data)) +func getAndExpectSuccess(ctx context.Context, uri string, api APIService) ([]byte, *models.Error) { + body, statusCode, status, err := get(ctx, uri, api) + if err != nil { + return nil, err + } + + if statusCode >= 200 && statusCode < 300 { + return body, nil + } + + if len(body) > 0 { + return nil, handleErrStatusCode(statusCode, body) + } + + return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", statusCode, status)) +} + +func get(ctx context.Context, uri string, api APIService) ([]byte, int, string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) + if err != nil { + return nil, 0, "", buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return nil, 0, "", buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, 0, "", buildErrorResponse(err.Error()) + } + + return body, resp.StatusCode, resp.Status, nil +} + +func putWithEventContext(ctx context.Context, uri string, data []byte, api APIService) (*models.EventContext, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "PUT", uri, bytes.NewBuffer(data)) if err != nil { return nil, buildErrorResponse(err.Error()) } @@ -84,20 +140,21 @@ func putWithEventContext(uri string, data []byte, api APIService) (*models.Event } if resp.StatusCode >= 200 && resp.StatusCode <= 204 { - if len(body) > 0 { - eventContext := &models.EventContext{} - if err = eventContext.FromJSON(body); err != nil { - // failed to parse json - return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) - } - - if eventContext.KeptnContext != nil { - fmt.Println("ID of Keptn context: " + *eventContext.KeptnContext) - } - return eventContext, nil + if len(body) == 0 { + return nil, nil + } + + eventContext := &models.EventContext{} + + if err = eventContext.FromJSON(body); err != nil { + // failed to parse json + return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) } - return nil, nil + if eventContext.KeptnContext != nil { + fmt.Println("ID of Keptn context: " + *eventContext.KeptnContext) + } + return eventContext, nil } if len(body) > 0 { @@ -107,8 +164,8 @@ func putWithEventContext(uri string, data []byte, api APIService) (*models.Event return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) } -func put(uri string, data []byte, api APIService) (string, *models.Error) { - req, err := http.NewRequest("PUT", uri, bytes.NewBuffer(data)) +func put(ctx context.Context, uri string, data []byte, api APIService) (string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "PUT", uri, bytes.NewBuffer(data)) if err != nil { return "", buildErrorResponse(err.Error()) } @@ -127,11 +184,7 @@ func put(uri string, data []byte, api APIService) (string, *models.Error) { } if resp.StatusCode >= 200 && resp.StatusCode <= 204 { - if len(body) > 0 { - return string(body), nil - } - - return "", nil + return string(body), nil } if len(body) > 0 { @@ -141,8 +194,8 @@ func put(uri string, data []byte, api APIService) (string, *models.Error) { return "", buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) } -func postWithEventContext(uri string, data []byte, api APIService) (*models.EventContext, *models.Error) { - req, err := http.NewRequest("POST", uri, bytes.NewBuffer(data)) +func postWithEventContext(ctx context.Context, uri string, data []byte, api APIService) (*models.EventContext, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(data)) if err != nil { return nil, buildErrorResponse(err.Error()) } @@ -161,20 +214,20 @@ func postWithEventContext(uri string, data []byte, api APIService) (*models.Even } if resp.StatusCode >= 200 && resp.StatusCode <= 204 { - if len(body) > 0 { - eventContext := &models.EventContext{} - if err = eventContext.FromJSON(body); err != nil { - // failed to parse json - return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) - } - - if eventContext.KeptnContext != nil { - fmt.Println("ID of Keptn context: " + *eventContext.KeptnContext) - } - return eventContext, nil + if len(body) == 0 { + return nil, nil + } + + eventContext := &models.EventContext{} + if err = eventContext.FromJSON(body); err != nil { + // failed to parse json + return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) } - return nil, nil + if eventContext.KeptnContext != nil { + fmt.Println("ID of Keptn context: " + *eventContext.KeptnContext) + } + return eventContext, nil } if len(body) > 0 { @@ -184,8 +237,8 @@ func postWithEventContext(uri string, data []byte, api APIService) (*models.Even return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) } -func post(uri string, data []byte, api APIService) (string, *models.Error) { - req, err := http.NewRequest("POST", uri, bytes.NewBuffer(data)) +func post(ctx context.Context, uri string, data []byte, api APIService) (string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(data)) if err != nil { return "", buildErrorResponse(err.Error()) } @@ -204,11 +257,7 @@ func post(uri string, data []byte, api APIService) (string, *models.Error) { } if resp.StatusCode >= 200 && resp.StatusCode <= 204 { - if len(body) > 0 { - return string(body), nil - } - - return "", nil + return string(body), nil } if len(body) > 0 { @@ -218,8 +267,8 @@ func post(uri string, data []byte, api APIService) (string, *models.Error) { return "", buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) } -func deleteWithEventContext(uri string, api APIService) (*models.EventContext, *models.Error) { - req, err := http.NewRequest("DELETE", uri, nil) +func deleteWithEventContext(ctx context.Context, uri string, api APIService) (*models.EventContext, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "DELETE", uri, nil) if err != nil { return nil, buildErrorResponse(err.Error()) } @@ -238,23 +287,23 @@ func deleteWithEventContext(uri string, api APIService) (*models.EventContext, * } if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if len(body) > 0 { - eventContext := &models.EventContext{} - if err = eventContext.FromJSON(body); err != nil { - // failed to parse json - return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) - } - return eventContext, nil + if len(body) == 0 { + return nil, nil } - return nil, nil + eventContext := &models.EventContext{} + if err = eventContext.FromJSON(body); err != nil { + // failed to parse json + return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) + } + return eventContext, nil } return nil, handleErrStatusCode(resp.StatusCode, body) } -func delete(uri string, api APIService) (string, *models.Error) { - req, err := http.NewRequest("DELETE", uri, nil) +func delete(ctx context.Context, uri string, api APIService) (string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "DELETE", uri, nil) if err != nil { return "", buildErrorResponse(err.Error()) } @@ -273,11 +322,7 @@ func delete(uri string, api APIService) (string, *models.Error) { } if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if len(body) > 0 { - return string(body), nil - } - - return "", nil + return string(body), nil } return "", handleErrStatusCode(resp.StatusCode, body) diff --git a/pkg/api/utils/apiUtils.go b/pkg/api/utils/apiUtils.go index c5f32b46..bc719b51 100644 --- a/pkg/api/utils/apiUtils.go +++ b/pkg/api/utils/apiUtils.go @@ -1,29 +1,47 @@ package api import ( - "io/ioutil" + "context" "net/http" "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) const v1EventPath = "/v1/event" const v1MetadataPath = "/v1/metadata" type APIV1Interface interface { + // SendEvent sends an event to Keptn. SendEvent(event models.KeptnContextExtendedCE) (*models.EventContext, *models.Error) + + // TriggerEvaluation triggers a new evaluation. TriggerEvaluation(project string, stage string, service string, evaluation models.Evaluation) (*models.EventContext, *models.Error) + + // CreateProject creates a new project. CreateProject(project models.CreateProject) (string, *models.Error) + + // UpdateProject updates a project. UpdateProject(project models.CreateProject) (string, *models.Error) + + // DeleteProject deletes a project. DeleteProject(project models.Project) (*models.DeleteProjectResponse, *models.Error) + + // CreateService creates a new service. CreateService(project string, service models.CreateService) (string, *models.Error) + + // DeleteService deletes a service. DeleteService(project string, service string) (*models.DeleteServiceResponse, *models.Error) + + // GetMetadata retrieves Keptn metadata information. GetMetadata() (*models.Metadata, *models.Error) } // APIHandler handles projects type APIHandler struct { + apiHandler *v2.APIHandler BaseURL string AuthToken string AuthHeader string @@ -31,6 +49,21 @@ type APIHandler struct { Scheme string } +// NewAPIHandler returns a new APIHandler +func NewAPIHandler(baseURL string) *APIHandler { + return NewAPIHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewAPIHandlerWithHTTPClient returns a new APIHandler that uses the specified http.Client +func NewAPIHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *APIHandler { + return &APIHandler{ + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + apiHandler: v2.NewAPIHandlerWithHTTPClient(baseURL, httpClient), + } +} + // NewAuthenticatedAPIHandler returns a new APIHandler that authenticates at the api-service endpoint via the provided token // Deprecated: use APISet instead func NewAuthenticatedAPIHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *APIHandler { @@ -42,18 +75,19 @@ func NewAuthenticatedAPIHandler(baseURL string, authToken string, authHeader str } func createAuthenticatedAPIHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *APIHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") + v2APIHandler := v2.NewAuthenticatedAPIHandler(baseURL, authToken, authHeader, httpClient, scheme) + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } return &APIHandler{ - BaseURL: baseURL, + BaseURL: httputils.TrimHTTPScheme(baseURL), AuthHeader: authHeader, AuthToken: authToken, HTTPClient: httpClient, Scheme: scheme, + apiHandler: v2APIHandler, } } @@ -73,128 +107,62 @@ func (a *APIHandler) getHTTPClient() *http.Client { return a.HTTPClient } -// SendEvent sends an event to Keptn +// SendEvent sends an event to Keptn. func (a *APIHandler) SendEvent(event models.KeptnContextExtendedCE) (*models.EventContext, *models.Error) { - baseURL := a.getAPIServicePath() - - bodyStr, err := event.ToJSON() - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - return postWithEventContext(a.Scheme+"://"+baseURL+v1EventPath, bodyStr, a) + a.ensureHandlerIsSet() + return a.apiHandler.SendEvent(context.TODO(), event, v2.APISendEventOptions{}) } -// TriggerEvaluation triggers a new evaluation +// TriggerEvaluation triggers a new evaluation. func (a *APIHandler) TriggerEvaluation(project, stage, service string, evaluation models.Evaluation) (*models.EventContext, *models.Error) { - bodyStr, err := evaluation.ToJSON() - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - return postWithEventContext(a.Scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+service+"/evaluation", bodyStr, a) + a.ensureHandlerIsSet() + return a.apiHandler.TriggerEvaluation(context.TODO(), project, stage, service, evaluation, v2.APITriggerEvaluationOptions{}) } -// CreateProject creates a new project +// CreateProject creates a new project. func (a *APIHandler) CreateProject(project models.CreateProject) (string, *models.Error) { - bodyStr, err := project.ToJSON() - if err != nil { - return "", buildErrorResponse(err.Error()) - } - return post(a.Scheme+"://"+a.getBaseURL()+v1ProjectPath, bodyStr, a) + a.ensureHandlerIsSet() + return a.apiHandler.CreateProject(context.TODO(), project, v2.APICreateProjectOptions{}) } -// UpdateProject updates project +// UpdateProject updates a project. func (a *APIHandler) UpdateProject(project models.CreateProject) (string, *models.Error) { - bodyStr, err := project.ToJSON() - if err != nil { - return "", buildErrorResponse(err.Error()) - } - return put(a.Scheme+"://"+a.getBaseURL()+v1ProjectPath, bodyStr, a) + a.ensureHandlerIsSet() + return a.apiHandler.UpdateProject(context.TODO(), project, v2.APIUpdateProjectOptions{}) } -// DeleteProject deletes a project +// DeleteProject deletes a project. func (a *APIHandler) DeleteProject(project models.Project) (*models.DeleteProjectResponse, *models.Error) { - resp, err := delete(a.Scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, a) - if err != nil { - return nil, err - } - deletePrjResponse := &models.DeleteProjectResponse{} - if err2 := deletePrjResponse.FromJSON([]byte(resp)); err2 != nil { - msg := "Could not decode DeleteProjectResponse: " + err2.Error() - return nil, &models.Error{ - Message: &msg, - } - } - return deletePrjResponse, nil + a.ensureHandlerIsSet() + return a.apiHandler.DeleteProject(context.TODO(), project, v2.APIDeleteProjectOptions{}) } -// CreateService creates a new service +// CreateService creates a new service. func (a *APIHandler) CreateService(project string, service models.CreateService) (string, *models.Error) { - bodyStr, err := service.ToJSON() - if err != nil { - return "", buildErrorResponse(err.Error()) - } - return post(a.Scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project+pathToService, bodyStr, a) + a.ensureHandlerIsSet() + return a.apiHandler.CreateService(context.TODO(), project, service, v2.APICreateServiceOptions{}) } -// DeleteProject deletes a project +// DeleteService deletes a service. func (a *APIHandler) DeleteService(project, service string) (*models.DeleteServiceResponse, *models.Error) { - resp, err := delete(a.Scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project+pathToService+"/"+service, a) - if err != nil { - return nil, err - } - deleteSvcResponse := &models.DeleteServiceResponse{} - if err2 := deleteSvcResponse.FromJSON([]byte(resp)); err2 != nil { - msg := "Could not decode DeleteServiceResponse: " + err2.Error() - return nil, &models.Error{ - Message: &msg, - } - } - return deleteSvcResponse, nil + a.ensureHandlerIsSet() + return a.apiHandler.DeleteService(context.TODO(), project, service, v2.APIDeleteServiceOptions{}) } -// GetMetadata retrieve keptn MetaData information +// GetMetadata retrieves Keptn metadata information. func (a *APIHandler) GetMetadata() (*models.Metadata, *models.Error) { - baseURL := a.getAPIServicePath() - - req, err := http.NewRequest("GET", a.Scheme+"://"+baseURL+v1MetadataPath, nil) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, a) - - resp, err := a.getHTTPClient().Do(req) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - - if len(body) > 0 { - respMetadata := &models.Metadata{} - if err = respMetadata.FromJSON(body); err != nil { - return nil, buildErrorResponse(err.Error()) - } - - return respMetadata, nil - } + a.ensureHandlerIsSet() + return a.apiHandler.GetMetadata(context.TODO(), v2.APIGetMetadataOptions{}) +} - return nil, nil +func (a *APIHandler) ensureHandlerIsSet() { + if a.apiHandler != nil { + return } - return nil, handleErrStatusCode(resp.StatusCode, body) -} - -func (a *APIHandler) getAPIServicePath() string { - baseURL := a.getBaseURL() - if strings.HasSuffix(baseURL, "/"+shipyardControllerBaseURL) { - baseURL = strings.TrimSuffix(a.getBaseURL(), "/"+shipyardControllerBaseURL) + if a.AuthToken != "" { + a.apiHandler = v2.NewAuthenticatedAPIHandler(a.BaseURL, a.AuthToken, a.AuthHeader, a.HTTPClient, a.Scheme) + } else { + a.apiHandler = v2.NewAPIHandlerWithHTTPClient(a.BaseURL, a.HTTPClient) } - return baseURL } diff --git a/pkg/api/utils/authUtils.go b/pkg/api/utils/authUtils.go index e51b8c6e..c7588c12 100644 --- a/pkg/api/utils/authUtils.go +++ b/pkg/api/utils/authUtils.go @@ -1,38 +1,41 @@ package api import ( + "context" "net/http" - "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) type AuthV1Interface interface { + // Authenticate authenticates the client request against the server. Authenticate() (*models.EventContext, *models.Error) } // AuthHandler handles projects type AuthHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + authHandler *v2.AuthHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } // NewAuthHandler returns a new AuthHandler func NewAuthHandler(baseURL string) *AuthHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewAuthHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewAuthHandlerWithHTTPClient returns a new AuthHandler that uses the specified http.Client +func NewAuthHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *AuthHandler { return &AuthHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + authHandler: v2.NewAuthHandlerWithHTTPClient(baseURL, httpClient), } } @@ -48,14 +51,13 @@ func NewAuthenticatedAuthHandler(baseURL string, authToken string, authHeader st } func createAuthenticatedAuthHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *AuthHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") return &AuthHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + authHandler: v2.NewAuthenticatedAuthHandler(baseURL, authToken, authHeader, httpClient, scheme), } } @@ -75,7 +77,20 @@ func (a *AuthHandler) getHTTPClient() *http.Client { return a.HTTPClient } -// Authenticate authenticates the client request against the server +// Authenticate authenticates the client request against the server. func (a *AuthHandler) Authenticate() (*models.EventContext, *models.Error) { - return postWithEventContext(a.Scheme+"://"+a.getBaseURL()+"/v1/auth", nil, a) + a.ensureHandlerIsSet() + return a.authHandler.Authenticate(context.TODO(), v2.AuthAuthenticateOptions{}) +} + +func (a *AuthHandler) ensureHandlerIsSet() { + if a.authHandler != nil { + return + } + + if a.AuthToken != "" { + a.authHandler = v2.NewAuthenticatedAuthHandler(a.BaseURL, a.AuthToken, a.AuthHeader, a.HTTPClient, a.Scheme) + } else { + a.authHandler = v2.NewAuthHandlerWithHTTPClient(a.BaseURL, a.HTTPClient) + } } diff --git a/pkg/api/utils/client.go b/pkg/api/utils/client.go index 775da78f..7bf78b4e 100644 --- a/pkg/api/utils/client.go +++ b/pkg/api/utils/client.go @@ -169,7 +169,7 @@ func New(baseURL string, options ...func(*APISet)) (*APISet, error) { as.authHandler = createAuthenticatedAuthHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) as.logHandler = createAuthenticatedLogHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) as.eventHandler = createAuthenticatedEventHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) - as.projectHandler = createAuthProjectHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.projectHandler = createAuthenticatedProjectHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) as.resourceHandler = createAuthenticatedResourceHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) as.secretHandler = createAuthenticatedSecretHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) as.sequenceControlHandler = createAuthenticatedSequenceControlHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) diff --git a/pkg/api/utils/eventUtils.go b/pkg/api/utils/eventUtils.go index 4dfc1e45..55e25dfd 100644 --- a/pkg/api/utils/eventUtils.go +++ b/pkg/api/utils/eventUtils.go @@ -1,30 +1,32 @@ package api import ( - "fmt" - "io/ioutil" - "log" + "context" "net/http" - "net/url" - "strconv" "strings" "time" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) type EventsV1Interface interface { + // GetEvents returns all events matching the properties in the passed filter object. GetEvents(filter *EventFilter) ([]*models.KeptnContextExtendedCE, *models.Error) + + // GetEventsWithRetry tries to retrieve events matching the passed filter. GetEventsWithRetry(filter *EventFilter, maxRetries int, retrySleepTime time.Duration) ([]*models.KeptnContextExtendedCE, error) } // EventHandler handles services type EventHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + eventHandler *v2.EventHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } // EventFilter allows to filter events based on the provided properties @@ -42,17 +44,16 @@ type EventFilter struct { // NewEventHandler returns a new EventHandler func NewEventHandler(baseURL string) *EventHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewEventHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewEventHandlerWithHTTPClient returns a new EventHandler that uses the specified http.Client +func NewEventHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *EventHandler { return &EventHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + eventHandler: v2.NewEventHandlerWithHTTPClient(baseURL, httpClient), } } @@ -69,19 +70,20 @@ func NewAuthenticatedEventHandler(baseURL string, authToken string, authHeader s } func createAuthenticatedEventHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *EventHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") + v2EventHandler := v2.NewAuthenticatedEventHandler(baseURL, authToken, authHeader, httpClient, scheme) + baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, mongodbDatastoreServiceBaseUrl) { baseURL += "/" + mongodbDatastoreServiceBaseUrl } return &EventHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + eventHandler: v2EventHandler, } } @@ -101,112 +103,40 @@ func (e *EventHandler) getHTTPClient() *http.Client { return e.HTTPClient } -// GetEvents returns all events matching the properties in the passed filter object +// GetEvents returns all events matching the properties in the passed filter object. func (e *EventHandler) GetEvents(filter *EventFilter) ([]*models.KeptnContextExtendedCE, *models.Error) { - - u, err := url.Parse(e.Scheme + "://" + e.getBaseURL() + "/event?") - if err != nil { - log.Fatal("error parsing url") - } - - query := u.Query() - - if filter.Project != "" { - query.Set("project", filter.Project) - } - if filter.Stage != "" { - query.Set("stage", filter.Stage) - } - if filter.Service != "" { - query.Set("service", filter.Service) - } - if filter.KeptnContext != "" { - query.Set("keptnContext", filter.KeptnContext) - } - if filter.EventID != "" { - query.Set("eventID", filter.EventID) - } - if filter.EventType != "" { - query.Set("type", filter.EventType) - } - if filter.PageSize != "" { - query.Set("pageSize", filter.PageSize) - } - if filter.FromTime != "" { - query.Set("fromTime", filter.FromTime) - } - - u.RawQuery = query.Encode() - - return e.getEvents(u.String(), filter.NumberOfPages) + e.ensureHandlerIsSet() + return e.eventHandler.GetEvents(context.TODO(), toV2EventFilter(filter), v2.EventsGetEventsOptions{}) } -// GetEventsWithRetry tries to retrieve events matching the passed filter +// GetEventsWithRetry tries to retrieve events matching the passed filter. func (e *EventHandler) GetEventsWithRetry(filter *EventFilter, maxRetries int, retrySleepTime time.Duration) ([]*models.KeptnContextExtendedCE, error) { - for i := 0; i < maxRetries; i = i + 1 { - events, errObj := e.GetEvents(filter) - if errObj == nil && len(events) > 0 { - return events, nil - } - <-time.After(retrySleepTime) + e.ensureHandlerIsSet() + return e.eventHandler.GetEventsWithRetry(context.TODO(), toV2EventFilter(filter), maxRetries, retrySleepTime, v2.EventsGetEventsWithRetryOptions{}) +} + +func toV2EventFilter(filter *EventFilter) *v2.EventFilter { + return &v2.EventFilter{ + Project: filter.Project, + Stage: filter.Stage, + Service: filter.Service, + EventType: filter.EventType, + KeptnContext: filter.KeptnContext, + EventID: filter.EventID, + PageSize: filter.PageSize, + NumberOfPages: filter.NumberOfPages, + FromTime: filter.FromTime, } - return nil, fmt.Errorf("could not find matching event after %d x %s", maxRetries, retrySleepTime.String()) } -func (e *EventHandler) getEvents(uri string, numberOfPages int) ([]*models.KeptnContextExtendedCE, *models.Error) { - events := []*models.KeptnContextExtendedCE{} - nextPageKey := "" - - for { - url, err := url.Parse(uri) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - q := url.Query() - if nextPageKey != "" { - q.Set("nextPageKey", nextPageKey) - url.RawQuery = q.Encode() - } - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, e) - - resp, err := e.HTTPClient.Do(req) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - - if resp.StatusCode == 200 { - received := &models.Events{} - if err = received.FromJSON(body); err != nil { - return nil, buildErrorResponse(err.Error()) - } - events = append(events, received.Events...) - - if received.NextPageKey == "" || received.NextPageKey == "0" { - break - } - - nextPageKeyInt, _ := strconv.Atoi(received.NextPageKey) - - if numberOfPages > 0 && nextPageKeyInt >= numberOfPages { - break - } - - nextPageKey = received.NextPageKey - } else { - return nil, handleErrStatusCode(resp.StatusCode, body) - } +func (e *EventHandler) ensureHandlerIsSet() { + if e.eventHandler != nil { + return } - return events, nil + if e.AuthToken != "" { + e.eventHandler = v2.NewAuthenticatedEventHandler(e.BaseURL, e.AuthToken, e.AuthHeader, e.HTTPClient, e.Scheme) + } else { + e.eventHandler = v2.NewEventHandlerWithHTTPClient(e.BaseURL, e.HTTPClient) + } } diff --git a/pkg/api/utils/logUtils.go b/pkg/api/utils/logUtils.go index c60457f0..d3279554 100644 --- a/pkg/api/utils/logUtils.go +++ b/pkg/api/utils/logUtils.go @@ -2,18 +2,15 @@ package api import ( "context" - "errors" - "fmt" - "io/ioutil" - "log" "net/http" - "net/url" "strings" "sync" "time" "github.com/benbjohnson/clock" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) const v1LogPath = "/v1/log" @@ -26,14 +23,24 @@ type LogsV1Interface interface { //go:generate moq -pkg utils_mock -skip-ensure -out ./fake/log_handler_mock.go . ILogHandler type ILogHandler interface { + + // Log appends the specified logs to the log cache. Log(logs []models.LogEntry) + + // Flush flushes the log cache. Flush() error + + // GetLogs gets logs with the specified parameters. GetLogs(params models.GetLogsParams) (*models.GetLogsResponse, error) + + // DeleteLogs deletes logs matching the specified log filter. DeleteLogs(filter models.LogFilter) error + Start(ctx context.Context) } type LogHandler struct { + logHandler *v2.LogHandler BaseURL string AuthToken string AuthHeader string @@ -45,21 +52,21 @@ type LogHandler struct { lock sync.Mutex } +// NewLogHandler returns a new LogHandler func NewLogHandler(baseURL string) *LogHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewLogHandlerWithHTTPClient(baseURL, &http.Client{Transport: getClientTransport(nil)}) +} + +// NewLogHandlerWithHTTPClient returns a new LogHandler that uses the specified http.Client +func NewLogHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *LogHandler { return &LogHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: getClientTransport(nil)}, + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, Scheme: "http", LogCache: []models.LogEntry{}, TheClock: clock.New(), SyncInterval: defaultSyncInterval, + logHandler: v2.NewLogHandlerWithHTTPClient(baseURL, httpClient), } } @@ -74,15 +81,15 @@ func NewAuthenticatedLogHandler(baseURL string, authToken string, authHeader str } func createAuthenticatedLogHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *LogHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") + v2LogHandler := v2.NewAuthenticatedLogHandler(baseURL, authToken, authHeader, httpClient, scheme) + baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } return &LogHandler{ - BaseURL: baseURL, + BaseURL: httputils.TrimHTTPScheme(baseURL), AuthHeader: authHeader, AuthToken: authToken, HTTPClient: httpClient, @@ -90,6 +97,7 @@ func createAuthenticatedLogHandler(baseURL string, authToken string, authHeader LogCache: []models.LogEntry{}, TheClock: clock.New(), SyncInterval: defaultSyncInterval, + logHandler: v2LogHandler, } } @@ -109,118 +117,43 @@ func (lh *LogHandler) getHTTPClient() *http.Client { return lh.HTTPClient } +// Log appends the specified logs to the log cache. func (lh *LogHandler) Log(logs []models.LogEntry) { - lh.lock.Lock() - defer lh.lock.Unlock() - lh.LogCache = append(lh.LogCache, logs...) + lh.ensureHandlerIsSet() + lh.logHandler.Log(logs, v2.LogsLogOptions{}) } +// GetLogs gets logs with the specified parameters. func (lh *LogHandler) GetLogs(params models.GetLogsParams) (*models.GetLogsResponse, error) { - u, err := url.Parse(lh.Scheme + "://" + lh.getBaseURL() + v1LogPath) - if err != nil { - log.Fatal("error parsing url") - } - - query := u.Query() - - if params.IntegrationID != "" { - query.Set("integrationId", params.IntegrationID) - } - if params.PageSize != 0 { - query.Set("pageSize", fmt.Sprintf("%d", params.PageSize)) - } - if params.FromTime != "" { - query.Set("fromTime", params.FromTime) - } - if params.BeforeTime != "" { - query.Set("beforeTime", params.BeforeTime) - } - - u.RawQuery = query.Encode() - - req, err := http.NewRequest("GET", u.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, lh) - - resp, err := lh.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode == http.StatusOK { - received := &models.GetLogsResponse{} - if err := received.FromJSON(body); err != nil { - return nil, err - } - return received, nil - } - - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() + lh.ensureHandlerIsSet() + return lh.logHandler.GetLogs(context.TODO(), params, v2.LogsGetLogsOptions{}) } +// DeleteLogs deletes logs matching the specified log filter. func (lh *LogHandler) DeleteLogs(params models.LogFilter) error { - u, err := url.Parse(lh.Scheme + "://" + lh.getBaseURL() + v1LogPath) - if err != nil { - log.Fatal("error parsing url") - } - - query := u.Query() - - if params.IntegrationID != "" { - query.Set("integrationId", params.IntegrationID) - } - if params.FromTime != "" { - query.Set("fromTime", params.FromTime) - } - if params.BeforeTime != "" { - query.Set("beforeTime", params.BeforeTime) - } - if _, err := delete(u.String(), lh); err != nil { - return errors.New(err.GetMessage()) - } - return nil + lh.ensureHandlerIsSet() + return lh.logHandler.DeleteLogs(context.TODO(), params, v2.LogsDeleteLogsOptions{}) } func (lh *LogHandler) Start(ctx context.Context) { - ticker := lh.TheClock.Ticker(lh.SyncInterval) - go func() { - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - lh.Flush() - } - } - }() + lh.ensureHandlerIsSet() + lh.logHandler.Start(ctx, v2.LogsStartOptions{}) } +// Flush flushes the log cache. func (lh *LogHandler) Flush() error { - lh.lock.Lock() - defer lh.lock.Unlock() - if len(lh.LogCache) == 0 { - // only send a request if we actually have some logs to send - return nil - } - createLogsPayload := &models.CreateLogsRequest{ - Logs: lh.LogCache, - } - bodyStr, err := createLogsPayload.ToJSON() - if err != nil { - return err + lh.ensureHandlerIsSet() + return lh.logHandler.Flush(context.TODO(), v2.LogsFlushOptions{}) +} + +func (lh *LogHandler) ensureHandlerIsSet() { + if lh.logHandler != nil { + return } - if _, err := post(lh.Scheme+"://"+lh.getBaseURL()+v1LogPath, bodyStr, lh); err != nil { - return errors.New(err.GetMessage()) + + if lh.AuthToken != "" { + lh.logHandler = v2.NewAuthenticatedLogHandler(lh.BaseURL, lh.AuthToken, lh.AuthHeader, lh.HTTPClient, lh.Scheme) + } else { + lh.logHandler = v2.NewLogHandlerWithHTTPClient(lh.BaseURL, lh.HTTPClient) } - lh.LogCache = []models.LogEntry{} - return nil } diff --git a/pkg/api/utils/projectUtils.go b/pkg/api/utils/projectUtils.go index d3fbba0d..72426aad 100644 --- a/pkg/api/utils/projectUtils.go +++ b/pkg/api/utils/projectUtils.go @@ -1,45 +1,56 @@ package api import ( - "crypto/tls" - "io/ioutil" + "context" "net/http" - "net/url" "strings" - "github.com/keptn/go-utils/pkg/common/httputils" - "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) const v1ProjectPath = "/v1/project" type ProjectsV1Interface interface { + // CreateProject creates a new project. CreateProject(project models.Project) (*models.EventContext, *models.Error) + + // DeleteProject deletes a project. DeleteProject(project models.Project) (*models.EventContext, *models.Error) + + // GetProject returns a project. GetProject(project models.Project) (*models.Project, *models.Error) + + // GetAllProjects returns all projects. GetAllProjects() ([]*models.Project, error) + + // UpdateConfigurationServiceProject updates a configuration service project. UpdateConfigurationServiceProject(project models.Project) (*models.EventContext, *models.Error) } // ProjectHandler handles projects type ProjectHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + projectHandler *v2.ProjectHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } // NewProjectHandler returns a new ProjectHandler which sends all requests directly to the configuration-service func NewProjectHandler(baseURL string) *ProjectHandler { - baseURL = httputils.TrimHTTPScheme(baseURL) + return NewProjectHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewProjectHandlerWithHTTPClient returns a new ProjectHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewProjectHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ProjectHandler { return &ProjectHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + projectHandler: v2.NewProjectHandlerWithHTTPClient(baseURL, httpClient), } } @@ -51,24 +62,24 @@ func NewAuthenticatedProjectHandler(baseURL string, authToken string, authHeader httpClient = &http.Client{} } httpClient.Transport = wrapOtelTransport(getClientTransport(httpClient.Transport)) - return createAuthProjectHandler(baseURL, authToken, authHeader, httpClient, scheme) + return createAuthenticatedProjectHandler(baseURL, authToken, authHeader, httpClient, scheme) } -func createAuthProjectHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ProjectHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") - baseURL = strings.TrimRight(baseURL, "/") +func createAuthenticatedProjectHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ProjectHandler { + v2ProjectHandler := v2.NewAuthenticatedProjectHandler(baseURL, authToken, authHeader, httpClient, scheme) + baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } return &ProjectHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + projectHandler: v2ProjectHandler, } } @@ -88,119 +99,44 @@ func (p *ProjectHandler) getHTTPClient() *http.Client { return p.HTTPClient } -// CreateProject creates a new project +// CreateProject creates a new project. func (p *ProjectHandler) CreateProject(project models.Project) (*models.EventContext, *models.Error) { - bodyStr, err := project.ToJSON() - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - return postWithEventContext(p.Scheme+"://"+p.getBaseURL()+v1ProjectPath, bodyStr, p) + p.ensureHandlerIsSet() + return p.projectHandler.CreateProject(context.TODO(), project, v2.ProjectsCreateProjectOptions{}) } -// DeleteProject deletes a project +// DeleteProject deletes a project. func (p *ProjectHandler) DeleteProject(project models.Project) (*models.EventContext, *models.Error) { - return deleteWithEventContext(p.Scheme+"://"+p.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, p) + p.ensureHandlerIsSet() + return p.projectHandler.DeleteProject(context.TODO(), project, v2.ProjectsDeleteProjectOptions{}) } -// GetProject returns a project +// GetProject returns a project. func (p *ProjectHandler) GetProject(project models.Project) (*models.Project, *models.Error) { - return getProject(p.Scheme+"://"+p.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, p) + p.ensureHandlerIsSet() + return p.projectHandler.GetProject(context.TODO(), project, v2.ProjectsGetProjectOptions{}) } -// GetProjects returns a project +// GetAllProjects returns all projects. func (p *ProjectHandler) GetAllProjects() ([]*models.Project, error) { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - projects := []*models.Project{} - - nextPageKey := "" - - for { - url, err := url.Parse(p.Scheme + "://" + p.getBaseURL() + v1ProjectPath) - if err != nil { - return nil, err - } - q := url.Query() - if nextPageKey != "" { - q.Set("nextPageKey", nextPageKey) - url.RawQuery = q.Encode() - } - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, p) - - resp, err := p.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode == 200 { - - received := &models.Projects{} - if err = received.FromJSON(body); err != nil { - return nil, err - } - projects = append(projects, received.Projects...) - - if received.NextPageKey == "" || received.NextPageKey == "0" { - break - } - nextPageKey = received.NextPageKey - } else { - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() - } - } - - return projects, nil + p.ensureHandlerIsSet() + return p.projectHandler.GetAllProjects(context.TODO(), v2.ProjectsGetAllProjectsOptions{}) } -func getProject(uri string, api APIService) (*models.Project, *models.Error) { - - req, err := http.NewRequest("GET", uri, nil) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, api) - - resp, err := api.getHTTPClient().Do(req) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - - if resp.StatusCode >= 200 && resp.StatusCode < 300 { - if len(body) > 0 { - respProject := &models.Project{} - if err = respProject.FromJSON(body); err != nil { - return nil, buildErrorResponse(err.Error()) - } +// UpdateConfigurationServiceProject updates a configuration service project. +func (p *ProjectHandler) UpdateConfigurationServiceProject(project models.Project) (*models.EventContext, *models.Error) { + p.ensureHandlerIsSet() + return p.projectHandler.UpdateConfigurationServiceProject(context.TODO(), project, v2.ProjectsUpdateConfigurationServiceProjectOptions{}) +} - return respProject, nil - } - return nil, nil +func (p *ProjectHandler) ensureHandlerIsSet() { + if p.projectHandler != nil { + return } - return nil, handleErrStatusCode(resp.StatusCode, body) -} - -func (p *ProjectHandler) UpdateConfigurationServiceProject(project models.Project) (*models.EventContext, *models.Error) { - bodyStr, err := project.ToJSON() - if err != nil { - return nil, buildErrorResponse(err.Error()) + if p.AuthToken != "" { + p.projectHandler = v2.NewAuthenticatedProjectHandler(p.BaseURL, p.AuthToken, p.AuthHeader, p.HTTPClient, p.Scheme) + } else { + p.projectHandler = v2.NewProjectHandlerWithHTTPClient(p.BaseURL, p.HTTPClient) } - return putWithEventContext(p.Scheme+"://"+p.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, bodyStr, p) } diff --git a/pkg/api/utils/resourceUtils.go b/pkg/api/utils/resourceUtils.go index cb7883ca..308f2605 100644 --- a/pkg/api/utils/resourceUtils.go +++ b/pkg/api/utils/resourceUtils.go @@ -1,17 +1,15 @@ package api import ( - "bytes" - "crypto/tls" - b64 "encoding/base64" + "context" "encoding/json" - "errors" - "io/ioutil" "net/http" "net/url" "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) const pathToResource = "/resource" @@ -19,36 +17,84 @@ const pathToService = "/service" const pathToStage = "/stage" const configurationServiceBaseURL = "configuration-service" -var ResourceNotFoundError = errors.New("Resource not found") +var ResourceNotFoundError = v2.ResourceNotFoundError type ResourcesV1Interface interface { + // CreateResources creates a resource for the specified entity. CreateResources(project string, stage string, service string, resources []*models.Resource) (*models.EventContext, *models.Error) + + // CreateProjectResources creates multiple project resources. CreateProjectResources(project string, resources []*models.Resource) (string, error) + + // GetProjectResource retrieves a project resource from the configuration service. + // Deprecated: use GetResource instead. GetProjectResource(project string, resourceURI string) (*models.Resource, error) + + // UpdateProjectResource updates a project resource. + // Deprecated: use UpdateResource instead. UpdateProjectResource(project string, resource *models.Resource) (string, error) + + // DeleteProjectResource deletes a project resource. + // Deprecated: use DeleteResource instead. DeleteProjectResource(project string, resourceURI string) error + + // UpdateProjectResources updates multiple project resources. UpdateProjectResources(project string, resources []*models.Resource) (string, error) + + // CreateStageResources creates a stage resource. + // Deprecated: use CreateResource instead. CreateStageResources(project string, stage string, resources []*models.Resource) (string, error) + + // GetStageResource retrieves a stage resource from the configuration service. + // Deprecated: use GetResource instead. GetStageResource(project string, stage string, resourceURI string) (*models.Resource, error) + + // UpdateStageResource updates a stage resource. + // Deprecated: use UpdateResource instead. UpdateStageResource(project string, stage string, resource *models.Resource) (string, error) + + // UpdateStageResources updates multiple stage resources. + // Deprecated: use UpdateResource instead. UpdateStageResources(project string, stage string, resources []*models.Resource) (string, error) + + // DeleteStageResource deletes a stage resource. + // Deprecated: use DeleteResource instead. DeleteStageResource(project string, stage string, resourceURI string) error + + // CreateServiceResources creates a service resource. + // Deprecated: use CreateResource instead. CreateServiceResources(project string, stage string, service string, resources []*models.Resource) (string, error) + + // GetServiceResource retrieves a service resource from the configuration service. + // Deprecated: use GetResource instead. GetServiceResource(project string, stage string, service string, resourceURI string) (*models.Resource, error) + + // UpdateServiceResource updates a service resource. + // Deprecated: use UpdateResource instead. UpdateServiceResource(project string, stage string, service string, resource *models.Resource) (string, error) + + // UpdateServiceResources updates multiple service resources. UpdateServiceResources(project string, stage string, service string, resources []*models.Resource) (string, error) + + // DeleteServiceResource deletes a service resource. + // Deprecated: use DeleteResource instead. DeleteServiceResource(project string, stage string, service string, resourceURI string) error + + // GetAllStageResources returns a list of all resources. GetAllStageResources(project string, stage string) ([]*models.Resource, error) + + // GetAllServiceResources returns a list of all resources. GetAllServiceResources(project string, stage string, service string) ([]*models.Resource, error) } // ResourceHandler handles resources type ResourceHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + resourceHandler *v2.ResourceHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } type resourceRequest struct { @@ -161,17 +207,16 @@ func (r *resourceRequest) FromJSON(b []byte) error { // NewResourceHandler returns a new ResourceHandler which sends all requests directly to the configuration-service func NewResourceHandler(baseURL string) *ResourceHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewResourceHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewResourceHandlerWithHTTPClient returns a new ResourceHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewResourceHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ResourceHandler { return &ResourceHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + resourceHandler: v2.NewResourceHandlerWithHTTPClient(baseURL, httpClient), } } @@ -187,18 +232,20 @@ func NewAuthenticatedResourceHandler(baseURL string, authToken string, authHeade } func createAuthenticatedResourceHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ResourceHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") + v2ResourceHandler := v2.NewAuthenticatedResourceHandler(baseURL, authToken, authHeader, httpClient, scheme) + baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, configurationServiceBaseURL) { baseURL += "/" + configurationServiceBaseURL } + return &ResourceHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + resourceHandler: v2ResourceHandler, } } @@ -218,368 +265,151 @@ func (r *ResourceHandler) getHTTPClient() *http.Client { return r.HTTPClient } -// CreateResources creates a resource for the specified entity +// CreateResources creates a resource for the specified entity. func (r *ResourceHandler) CreateResources(project string, stage string, service string, resources []*models.Resource) (*models.EventContext, *models.Error) { - - copiedResources := make([]*models.Resource, len(resources), len(resources)) - for i, val := range resources { - resourceContent := b64.StdEncoding.EncodeToString([]byte(val.ResourceContent)) - copiedResources[i] = &models.Resource{ResourceURI: val.ResourceURI, ResourceContent: resourceContent} - } - - resReq := &resourceRequest{ - Resources: copiedResources, - } - requestStr, err := resReq.ToJSON() - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - - if project != "" && stage != "" && service != "" { - return postWithEventContext(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+service+pathToResource, requestStr, r) - } else if project != "" && stage != "" && service == "" { - return postWithEventContext(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource, requestStr, r) - } else { - return postWithEventContext(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+"/"+pathToResource, requestStr, r) - } + r.ensureHandlerIsSet() + return r.resourceHandler.CreateResources(context.TODO(), project, stage, service, resources, v2.ResourcesCreateResourcesOptions{}) } -// CreateProjectResources creates multiple project resources +// CreateProjectResources creates multiple project resources. func (r *ResourceHandler) CreateProjectResources(project string, resources []*models.Resource) (string, error) { - return r.createResources(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToResource, resources) + r.ensureHandlerIsSet() + return r.resourceHandler.CreateProjectResources(context.TODO(), project, resources, v2.ResourcesCreateProjectResourcesOptions{}) } -// GetProjectResource retrieves a project resource from the configuration service -// Deprecated: use GetResource instead +// GetProjectResource retrieves a project resource from the configuration service. +// Deprecated: use GetResource instead. func (r *ResourceHandler) GetProjectResource(project string, resourceURI string) (*models.Resource, error) { + r.ensureHandlerIsSet() buildURI := r.Scheme + "://" + r.BaseURL + v1ProjectPath + "/" + project + pathToResource + "/" + url.QueryEscape(resourceURI) - return r.getResource(buildURI) + return r.resourceHandler.GetResourceByURI(context.TODO(), buildURI) } -// UpdateProjectResource updates a project resource -// Deprecated: use UpdateResource instead +// UpdateProjectResource updates a project resource. +// Deprecated: use UpdateResource instead. func (r *ResourceHandler) UpdateProjectResource(project string, resource *models.Resource) (string, error) { - return r.updateResource(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToResource+"/"+url.QueryEscape(*resource.ResourceURI), resource) + r.ensureHandlerIsSet() + return r.resourceHandler.UpdateResourceByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToResource+"/"+url.QueryEscape(*resource.ResourceURI), resource) } -// DeleteProjectResource deletes a project resource -// Deprecated: use DeleteResource instead +// DeleteProjectResource deletes a project resource. +// Deprecated: use DeleteResource instead. func (r *ResourceHandler) DeleteProjectResource(project string, resourceURI string) error { - return r.deleteResource(r.Scheme + "://" + r.BaseURL + v1ProjectPath + "/" + project + pathToResource + "/" + url.QueryEscape(resourceURI)) + r.ensureHandlerIsSet() + return r.resourceHandler.DeleteResourceByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToResource+"/"+url.QueryEscape(resourceURI)) } -// UpdateProjectResources updates multiple project resources +// UpdateProjectResources updates multiple project resources. func (r *ResourceHandler) UpdateProjectResources(project string, resources []*models.Resource) (string, error) { - return r.updateResources(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToResource, resources) + r.ensureHandlerIsSet() + return r.resourceHandler.UpdateProjectResources(context.TODO(), project, resources, v2.ResourcesUpdateProjectResourcesOptions{}) } -// CreateStageResources creates a stage resource -// Deprecated: use CreateResource instead +// CreateStageResources creates a stage resource. +// Deprecated: use CreateResource instead. func (r *ResourceHandler) CreateStageResources(project string, stage string, resources []*models.Resource) (string, error) { - return r.createResources(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource, resources) + r.ensureHandlerIsSet() + return r.resourceHandler.CreateResourcesByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource, resources) } -// GetStageResource retrieves a stage resource from the configuration service -// Deprecated: use GetResource instead +// GetStageResource retrieves a stage resource from the configuration service. +// Deprecated: use GetResource instead. func (r *ResourceHandler) GetStageResource(project string, stage string, resourceURI string) (*models.Resource, error) { + r.ensureHandlerIsSet() buildURI := r.Scheme + "://" + r.BaseURL + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToResource + "/" + url.QueryEscape(resourceURI) - return r.getResource(buildURI) + return r.resourceHandler.GetResourceByURI(context.TODO(), buildURI) } -// UpdateStageResource updates a stage resource -// Deprecated: use UpdateResource instead +// UpdateStageResource updates a stage resource. +// Deprecated: use UpdateResource instead. func (r *ResourceHandler) UpdateStageResource(project string, stage string, resource *models.Resource) (string, error) { - return r.updateResource(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource+"/"+url.QueryEscape(*resource.ResourceURI), resource) + r.ensureHandlerIsSet() + return r.resourceHandler.UpdateResourceByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource+"/"+url.QueryEscape(*resource.ResourceURI), resource) } -// UpdateStageResources updates multiple stage resources -// Deprecated: use UpdateResource instead +// UpdateStageResources updates multiple stage resources. +// Deprecated: use UpdateResource instead. func (r *ResourceHandler) UpdateStageResources(project string, stage string, resources []*models.Resource) (string, error) { - return r.updateResources(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource, resources) + r.ensureHandlerIsSet() + return r.resourceHandler.UpdateResourcesByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource, resources) } -// DeleteStageResource deletes a stage resource -// Deprecated: use DeleteResource instead +// DeleteStageResource deletes a stage resource. +// Deprecated: use DeleteResource instead. func (r *ResourceHandler) DeleteStageResource(project string, stage string, resourceURI string) error { - return r.deleteResource(r.Scheme + "://" + r.BaseURL + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToResource + "/" + url.QueryEscape(resourceURI)) + r.ensureHandlerIsSet() + return r.resourceHandler.DeleteResourceByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource+"/"+url.QueryEscape(resourceURI)) } -// CreateServiceResources creates a service resource -// Deprecated: use CreateResource instead +// CreateServiceResources creates a service resource. +// Deprecated: use CreateResource instead. func (r *ResourceHandler) CreateServiceResources(project string, stage string, service string, resources []*models.Resource) (string, error) { - return r.createResources(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+service+pathToResource, resources) + r.ensureHandlerIsSet() + return r.resourceHandler.CreateResourcesByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+service+pathToResource, resources) } -// GetServiceResource retrieves a service resource from the configuration service -// Deprecated: use GetResource instead +// GetServiceResource retrieves a service resource from the configuration service. +// Deprecated: use GetResource instead. func (r *ResourceHandler) GetServiceResource(project string, stage string, service string, resourceURI string) (*models.Resource, error) { + r.ensureHandlerIsSet() buildURI := r.Scheme + "://" + r.BaseURL + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToService + "/" + url.QueryEscape(service) + pathToResource + "/" + url.QueryEscape(resourceURI) - return r.getResource(buildURI) + return r.resourceHandler.GetResourceByURI(context.TODO(), buildURI) } -// UpdateServiceResource updates a service resource -// Deprecated: use UpdateResource instead +// UpdateServiceResource updates a service resource. +// Deprecated: use UpdateResource instead. func (r *ResourceHandler) UpdateServiceResource(project string, stage string, service string, resource *models.Resource) (string, error) { - return r.updateResource(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+url.QueryEscape(service)+pathToResource+"/"+url.QueryEscape(*resource.ResourceURI), resource) + r.ensureHandlerIsSet() + return r.resourceHandler.UpdateResourceByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+url.QueryEscape(service)+pathToResource+"/"+url.QueryEscape(*resource.ResourceURI), resource) } -// UpdateServiceResources updates multiple service resources +// UpdateServiceResources updates multiple service resources. func (r *ResourceHandler) UpdateServiceResources(project string, stage string, service string, resources []*models.Resource) (string, error) { - return r.updateResources(r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+url.QueryEscape(service)+pathToResource, resources) + r.ensureHandlerIsSet() + return r.resourceHandler.UpdateServiceResources(context.TODO(), project, stage, service, resources, v2.ResourcesUpdateServiceResourcesOptions{}) } -// DeleteServiceResource deletes a service resource -// Deprecated: use DeleteResource instead +// DeleteServiceResource deletes a service resource. +// Deprecated: use DeleteResource instead. func (r *ResourceHandler) DeleteServiceResource(project string, stage string, service string, resourceURI string) error { - return r.deleteResource(r.Scheme + "://" + r.BaseURL + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToService + "/" + url.QueryEscape(service) + pathToResource + "/" + url.QueryEscape(resourceURI)) + r.ensureHandlerIsSet() + return r.resourceHandler.DeleteResourceByURI(context.TODO(), r.Scheme+"://"+r.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+url.QueryEscape(service)+pathToResource+"/"+url.QueryEscape(resourceURI)) } -func (r *ResourceHandler) createResources(uri string, resources []*models.Resource) (string, error) { - return r.writeResources(uri, "POST", resources) -} - -func (r *ResourceHandler) updateResources(uri string, resources []*models.Resource) (string, error) { - return r.writeResources(uri, "PUT", resources) -} - -func (r *ResourceHandler) writeResources(uri string, method string, resources []*models.Resource) (string, error) { - - copiedResources := make([]*models.Resource, len(resources), len(resources)) - for i, val := range resources { - copiedResources[i] = &models.Resource{ResourceURI: val.ResourceURI, ResourceContent: b64.StdEncoding.EncodeToString([]byte(val.ResourceContent))} - } - resReq := &resourceRequest{ - Resources: copiedResources, - } - - resourceStr, err := resReq.ToJSON() - if err != nil { - return "", err - } - req, err := http.NewRequest(method, uri, bytes.NewBuffer(resourceStr)) - if err != nil { - return "", err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, r) - - resp, err := r.HTTPClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - version := &models.Version{} - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { - return "", errors.New(string(body)) - } - - if err = version.FromJSON(body); err != nil { - return "", err - } - - return version.Version, nil -} - -func (r *ResourceHandler) updateResource(uri string, resource *models.Resource) (string, error) { - return r.writeResource(uri, "PUT", resource) -} - -func (r *ResourceHandler) writeResource(uri string, method string, resource *models.Resource) (string, error) { - - copiedResource := &models.Resource{ResourceURI: resource.ResourceURI, ResourceContent: b64.StdEncoding.EncodeToString([]byte(resource.ResourceContent))} - - resourceStr, err := copiedResource.ToJSON() - if err != nil { - return "", err - } - req, err := http.NewRequest(method, uri, bytes.NewBuffer(resourceStr)) - if err != nil { - return "", err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, r) - - resp, err := r.HTTPClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return "", err - } - - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { - return "", errors.New(string(body)) - } - - version := &models.Version{} - if err = version.FromJSON(body); err != nil { - return "", err - } - - return version.Version, nil -} - -//GetResource returns a resource from the defined ResourceScope after applying all URI change configured in the options +//GetResource returns a resource from the defined ResourceScope after applying all URI change configured in the options. func (r *ResourceHandler) GetResource(scope ResourceScope, options ...URIOption) (*models.Resource, error) { - buildURI := r.buildResourceURI(scope) - return r.getResource(r.applyOptions(buildURI, options)) + r.ensureHandlerIsSet() + return r.resourceHandler.GetResource(context.TODO(), toV2ResourceScope(scope), v2.ResourcesGetResourceOptions{URIOptions: toV2URIOptions(options)}) } -//DeleteResource delete a resource from the URI defined by ResourceScope and modified by the URIOption +//DeleteResource delete a resource from the URI defined by ResourceScope and modified by the URIOption. func (r *ResourceHandler) DeleteResource(scope ResourceScope, options ...URIOption) error { - buildURI := r.buildResourceURI(scope) - return r.deleteResource(r.applyOptions(buildURI, options)) + r.ensureHandlerIsSet() + return r.resourceHandler.DeleteResource(context.TODO(), toV2ResourceScope(scope), v2.ResourcesDeleteResourceOptions{URIOptions: toV2URIOptions(options)}) } -//UpdateResource updates a resource from the URI defined by ResourceScope and modified by the URIOption +//UpdateResource updates a resource from the URI defined by ResourceScope and modified by the URIOption. func (r *ResourceHandler) UpdateResource(resource *models.Resource, scope ResourceScope, options ...URIOption) (string, error) { - buildURI := r.buildResourceURI(scope) - return r.updateResource(r.applyOptions(buildURI, options), resource) + r.ensureHandlerIsSet() + return r.resourceHandler.UpdateResource(context.TODO(), resource, toV2ResourceScope(scope), v2.ResourcesUpdateResourceOptions{URIOptions: toV2URIOptions(options)}) } -//CreateResource creates one or more resources at the URI defined by ResourceScope and modified by the URIOption +//CreateResource creates one or more resources at the URI defined by ResourceScope and modified by the URIOption. func (r *ResourceHandler) CreateResource(resource []*models.Resource, scope ResourceScope, options ...URIOption) (string, error) { - buildURI := r.buildResourceURI(scope) - return r.createResources(r.applyOptions(buildURI, options), resource) -} - -func (r *ResourceHandler) getResource(uri string) (*models.Resource, error) { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - req, err := http.NewRequest("GET", uri, nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, r) - - resp, err := r.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode == 404 { - // need to handle this case differently (e.g. https://github.com/keptn/keptn/issues/1480) - return nil, ResourceNotFoundError - } - if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { - return nil, errors.New(string(body)) - } - - resource := &models.Resource{} - if err = resource.FromJSON(body); err != nil { - return nil, err - } - - // decode resource content - decodedStr, err := b64.StdEncoding.DecodeString(resource.ResourceContent) - if err != nil { - return nil, err - } - resource.ResourceContent = string(decodedStr) - - return resource, nil -} - -func (r *ResourceHandler) deleteResource(uri string) error { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - req, err := http.NewRequest("DELETE", uri, nil) - if err != nil { - return err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, r) - - resp, err := r.HTTPClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - return nil + r.ensureHandlerIsSet() + return r.resourceHandler.CreateResource(context.TODO(), resource, toV2ResourceScope(scope), v2.ResourcesCreateResourceOptions{URIOptions: toV2URIOptions(options)}) } // GetAllStageResources returns a list of all resources. func (r *ResourceHandler) GetAllStageResources(project string, stage string) ([]*models.Resource, error) { - myURL, err := url.Parse(r.Scheme + "://" + r.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToResource) - if err != nil { - return nil, err - } - return r.getAllResources(myURL) + r.ensureHandlerIsSet() + return r.resourceHandler.GetAllStageResources(context.TODO(), project, stage, v2.ResourcesGetAllStageResourcesOptions{}) } // GetAllServiceResources returns a list of all resources. func (r *ResourceHandler) GetAllServiceResources(project string, stage string, service string) ([]*models.Resource, error) { - myURL, err := url.Parse(r.Scheme + "://" + r.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + - pathToService + "/" + service + pathToResource + "/") - if err != nil { - return nil, err - } - return r.getAllResources(myURL) -} - -func (r *ResourceHandler) getAllResources(u *url.URL) ([]*models.Resource, error) { - - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - resources := []*models.Resource{} - - nextPageKey := "" - - for { - if nextPageKey != "" { - q := u.Query() - q.Set("nextPageKey", nextPageKey) - u.RawQuery = q.Encode() - } - req, err := http.NewRequest("GET", u.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, r) - - resp, err := r.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode == 200 { - received := &models.Resources{} - if err = received.FromJSON(body); err != nil { - return nil, err - } - resources = append(resources, received.Resources...) - - if received.NextPageKey == "" || received.NextPageKey == "0" { - break - } - nextPageKey = received.NextPageKey - - } else { - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() - } - } - - return resources, nil + r.ensureHandlerIsSet() + return r.resourceHandler.GetAllServiceResources(context.TODO(), project, stage, service, v2.ResourcesGetAllServiceResourcesOptions{}) } func buildPath(base, name string) string { @@ -589,3 +419,27 @@ func buildPath(base, name string) string { } return path } + +func toV2URIOptions(uriOptions []URIOption) []v2.URIOption { + var v2URIOptions []v2.URIOption + for _, v := range uriOptions { + v2URIOptions = append(v2URIOptions, v2.URIOption(v)) + } + return v2URIOptions +} + +func toV2ResourceScope(scope ResourceScope) v2.ResourceScope { + return *(v2.NewResourceScope().Project(scope.project).Stage(scope.stage).Service(scope.service).Resource(scope.resource)) +} + +func (r *ResourceHandler) ensureHandlerIsSet() { + if r.resourceHandler != nil { + return + } + + if r.AuthToken != "" { + r.resourceHandler = v2.NewAuthenticatedResourceHandler(r.BaseURL, r.AuthToken, r.AuthHeader, r.HTTPClient, r.Scheme) + } else { + r.resourceHandler = v2.NewResourceHandlerWithHTTPClient(r.BaseURL, r.HTTPClient) + } +} diff --git a/pkg/api/utils/secretUtils.go b/pkg/api/utils/secretUtils.go index 9e389874..3e2308e3 100644 --- a/pkg/api/utils/secretUtils.go +++ b/pkg/api/utils/secretUtils.go @@ -1,12 +1,13 @@ package api import ( - "errors" - "io/ioutil" + "context" "net/http" "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) const secretServiceBaseURL = "secrets" @@ -18,34 +19,41 @@ type SecretsV1Interface interface { //go:generate moq -pkg utils_mock -skip-ensure -out ./fake/secret_handler_mock.go . SecretHandlerInterface type SecretHandlerInterface interface { + // CreateSecret creates a new secret. CreateSecret(secret models.Secret) error + + // UpdateSecret creates a new secret. UpdateSecret(secret models.Secret) error + + // DeleteSecret deletes a secret. DeleteSecret(secretName, secretScope string) error + + // GetSecrets returns a list of created secrets. GetSecrets() (*models.GetSecretsResponse, error) } // SecretHandler handles services type SecretHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + secretHandler *v2.SecretHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } // NewSecretHandler returns a new SecretHandler which sends all requests directly to the secret-service func NewSecretHandler(baseURL string) *SecretHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewSecretHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewSecretHandlerWithHTTPClient returns a new SecretHandler which sends all requests directly to the secret-service using the specified http.Client +func NewSecretHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *SecretHandler { return &SecretHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + secretHandler: v2.NewSecretHandlerWithHTTPClient(baseURL, httpClient), } } @@ -61,21 +69,20 @@ func NewAuthenticatedSecretHandler(baseURL string, authToken string, authHeader } func createAuthenticatedSecretHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *SecretHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") + v2SecretHandler := v2.NewAuthenticatedSecretHandler(baseURL, authToken, authHeader, httpClient, scheme) baseURL = strings.TrimRight(baseURL, "/") - if !strings.HasSuffix(baseURL, secretServiceBaseURL) { baseURL += "/" + secretServiceBaseURL } return &SecretHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + secretHandler: v2SecretHandler, } } @@ -95,67 +102,38 @@ func (s *SecretHandler) getHTTPClient() *http.Client { return s.HTTPClient } -// CreateSecret creates a new secret +// CreateSecret creates a new secret. func (s *SecretHandler) CreateSecret(secret models.Secret) error { - body, err := secret.ToJSON() - if err != nil { - return err - } - _, errObj := post(s.Scheme+"://"+s.BaseURL+v1SecretPath, body, s) - if errObj != nil { - return errors.New(errObj.GetMessage()) - } - return nil + s.ensureHandlerIsSet() + return s.secretHandler.CreateSecret(context.TODO(), secret, v2.SecretsCreateSecretOptions{}) } -// UpdateSecret creates a new secret +// UpdateSecret creates a new secret. func (s *SecretHandler) UpdateSecret(secret models.Secret) error { - body, err := secret.ToJSON() - if err != nil { - return err - } - _, errObj := put(s.Scheme+"://"+s.BaseURL+v1SecretPath, body, s) - if errObj != nil { - return errors.New(errObj.GetMessage()) - } - return nil + s.ensureHandlerIsSet() + return s.secretHandler.UpdateSecret(context.TODO(), secret, v2.SecretsUpdateSecretOptions{}) } -// DeleteSecret deletes a secret +// DeleteSecret deletes a secret. func (s *SecretHandler) DeleteSecret(secretName, secretScope string) error { - _, err := delete(s.Scheme+"://"+s.BaseURL+v1SecretPath+"?name="+secretName+"&scope="+secretScope, s) - if err != nil { - return errors.New(err.GetMessage()) - } - return nil + s.ensureHandlerIsSet() + return s.secretHandler.DeleteSecret(context.TODO(), secretName, secretScope, v2.SecretsDeleteSecretOptions{}) } -// GetSecrets returns a list of created secrets +// GetSecrets returns a list of created secrets. func (s *SecretHandler) GetSecrets() (*models.GetSecretsResponse, error) { - req, err := http.NewRequest("GET", s.Scheme+"://"+s.BaseURL+v1SecretPath, nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, s) - - resp, err := s.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + s.ensureHandlerIsSet() + return s.secretHandler.GetSecrets(context.TODO(), v2.SecretsGetSecretsOptions{}) +} - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err +func (s *SecretHandler) ensureHandlerIsSet() { + if s.secretHandler != nil { + return } - if resp.StatusCode != http.StatusOK { - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() - } - result := &models.GetSecretsResponse{} - if err := result.FromJSON(body); err != nil { - return nil, err + if s.AuthToken != "" { + s.secretHandler = v2.NewAuthenticatedSecretHandler(s.BaseURL, s.AuthToken, s.AuthHeader, s.HTTPClient, s.Scheme) + } else { + s.secretHandler = v2.NewSecretHandlerWithHTTPClient(s.BaseURL, s.HTTPClient) } - return result, nil } diff --git a/pkg/api/utils/sequenceUtils.go b/pkg/api/utils/sequenceUtils.go index 02a80878..b3ed68c2 100644 --- a/pkg/api/utils/sequenceUtils.go +++ b/pkg/api/utils/sequenceUtils.go @@ -1,11 +1,13 @@ package api import ( + "context" "encoding/json" "fmt" "net/http" "strings" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" "github.com/keptn/go-utils/pkg/common/httputils" ) @@ -16,11 +18,12 @@ type SequencesV1Interface interface { } type SequenceControlHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + sequenceControlHandler *v2.SequenceControlHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } type SequenceControlParams struct { @@ -69,14 +72,18 @@ func (s *SequenceControlBody) FromJSON(b []byte) error { return nil } +// NewSequenceControlHandler returns a new SequenceControlHandler func NewSequenceControlHandler(baseURL string) *SequenceControlHandler { - baseURL = httputils.TrimHTTPScheme(baseURL) + return NewSequenceControlHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewSequenceControlHandlerWithHTTPClient returns a new SequenceControlHandler using the specified http.Client +func NewSequenceControlHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *SequenceControlHandler { return &SequenceControlHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + sequenceControlHandler: v2.NewSequenceControlHandlerWithHTTPClient(baseURL, httpClient), } } @@ -91,20 +98,20 @@ func NewAuthenticatedSequenceControlHandler(baseURL string, authToken string, au } func createAuthenticatedSequenceControlHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *SequenceControlHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") - baseURL = strings.TrimRight(baseURL, "/") + v2SequenceControlHandler := v2.NewAuthenticatedSequenceControlHandler(baseURL, authToken, authHeader, httpClient, scheme) + baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } return &SequenceControlHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + sequenceControlHandler: v2SequenceControlHandler, } } @@ -125,28 +132,26 @@ func (s *SequenceControlHandler) getHTTPClient() *http.Client { } func (s *SequenceControlHandler) ControlSequence(params SequenceControlParams) error { - err := params.Validate() - if err != nil { - return err - } - - baseurl := fmt.Sprintf("%s://%s", s.Scheme, s.getBaseURL()) - path := fmt.Sprintf(v1SequenceControlPath, params.Project, params.KeptnContext) - - body := SequenceControlBody{ - Stage: params.Stage, - State: params.State, - } + s.ensureHandlerIsSet() + return s.sequenceControlHandler.ControlSequence( + context.TODO(), + v2.SequenceControlParams{ + Project: params.Project, + KeptnContext: params.KeptnContext, + Stage: params.Stage, + State: params.State, + }, + v2.SequencesControlSequenceOptions{}) +} - payload, err := body.ToJSON() - if err != nil { - return err +func (s *SequenceControlHandler) ensureHandlerIsSet() { + if s.sequenceControlHandler != nil { + return } - _, errResponse := post(baseurl+path, payload, s) - if errResponse != nil { - return fmt.Errorf(errResponse.GetMessage()) + if s.AuthToken != "" { + s.sequenceControlHandler = v2.NewAuthenticatedSequenceControlHandler(s.BaseURL, s.AuthToken, s.AuthHeader, s.HTTPClient, s.Scheme) + } else { + s.sequenceControlHandler = v2.NewSequenceControlHandlerWithHTTPClient(s.BaseURL, s.HTTPClient) } - - return nil } diff --git a/pkg/api/utils/serviceUtils.go b/pkg/api/utils/serviceUtils.go index c9b11989..b22be79f 100644 --- a/pkg/api/utils/serviceUtils.go +++ b/pkg/api/utils/serviceUtils.go @@ -1,44 +1,51 @@ package api import ( - "crypto/tls" - "io/ioutil" + "context" "net/http" - "net/url" "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) type ServicesV1Interface interface { + // CreateServiceInStage creates a new service. CreateServiceInStage(project string, stage string, serviceName string) (*models.EventContext, *models.Error) + + // DeleteServiceFromStage deletes a service from a stage. DeleteServiceFromStage(project string, stage string, serviceName string) (*models.EventContext, *models.Error) + + // GetService gets a service. GetService(project, stage, service string) (*models.Service, error) + + // GetAllServices returns a list of all services. GetAllServices(project string, stage string) ([]*models.Service, error) } // ServiceHandler handles services type ServiceHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + serviceHandler *v2.ServiceHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } // NewServiceHandler returns a new ServiceHandler which sends all requests directly to the configuration-service func NewServiceHandler(baseURL string) *ServiceHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewServiceHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewServiceHandlerWithHTTPClient returns a new ServiceHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewServiceHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ServiceHandler { return &ServiceHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + serviceHandler: v2.NewServiceHandlerWithHTTPClient(baseURL, httpClient), } } @@ -54,21 +61,20 @@ func NewAuthenticatedServiceHandler(baseURL string, authToken string, authHeader } func createAuthenticatedServiceHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ServiceHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") + v2ServiceHandler := v2.NewAuthenticatedServiceHandler(baseURL, authToken, authHeader, httpClient, scheme) baseURL = strings.TrimRight(baseURL, "/") - if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } return &ServiceHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + serviceHandler: v2ServiceHandler, } } @@ -88,109 +94,38 @@ func (s *ServiceHandler) getHTTPClient() *http.Client { return s.HTTPClient } -// CreateService creates a new service +// CreateServiceInStage creates a new service. func (s *ServiceHandler) CreateServiceInStage(project string, stage string, serviceName string) (*models.EventContext, *models.Error) { - - service := models.Service{ServiceName: serviceName} - body, err := service.ToJSON() - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - return postWithEventContext(s.Scheme+"://"+s.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService, body, s) + s.ensureHandlerIsSet() + return s.serviceHandler.CreateServiceInStage(context.TODO(), project, stage, serviceName, v2.ServicesCreateServiceInStageOptions{}) } -// DeleteServiceFromStage godoc +// DeleteServiceFromStage deletes a service from a stage. func (s *ServiceHandler) DeleteServiceFromStage(project string, stage string, serviceName string) (*models.EventContext, *models.Error) { - return deleteWithEventContext(s.Scheme+"://"+s.BaseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+serviceName, s) + s.ensureHandlerIsSet() + return s.serviceHandler.DeleteServiceFromStage(context.TODO(), project, stage, serviceName, v2.ServicesDeleteServiceFromStageOptions{}) } +// GetService gets a service. func (s *ServiceHandler) GetService(project, stage, service string) (*models.Service, error) { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - url, err := url.Parse(s.Scheme + "://" + s.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToService + "/" + service) - if err != nil { - return nil, err - } - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, s) - - resp, err := s.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode == 200 { - received := &models.Service{} - if err = received.FromJSON(body); err != nil { - return nil, err - } - return received, nil - } else { - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() - } + s.ensureHandlerIsSet() + return s.serviceHandler.GetService(context.TODO(), project, stage, service, v2.ServicesGetServiceOptions{}) } // GetAllServices returns a list of all services. func (s *ServiceHandler) GetAllServices(project string, stage string) ([]*models.Service, error) { + s.ensureHandlerIsSet() + return s.serviceHandler.GetAllServices(context.TODO(), project, stage, v2.ServicesGetAllServicesOptions{}) +} - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - services := []*models.Service{} - - nextPageKey := "" - - for { - url, err := url.Parse(s.Scheme + "://" + s.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToService) - if err != nil { - return nil, err - } - q := url.Query() - if nextPageKey != "" { - q.Set("nextPageKey", nextPageKey) - url.RawQuery = q.Encode() - } - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, s) - - resp, err := s.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode == 200 { - received := &models.Services{} - if err = received.FromJSON(body); err != nil { - return nil, err - } - services = append(services, received.Services...) - - if received.NextPageKey == "" || received.NextPageKey == "0" { - break - } - nextPageKey = received.NextPageKey - } else { - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() - } +func (s *ServiceHandler) ensureHandlerIsSet() { + if s.serviceHandler != nil { + return } - return services, nil + if s.AuthToken != "" { + s.serviceHandler = v2.NewAuthenticatedServiceHandler(s.BaseURL, s.AuthToken, s.AuthHeader, s.HTTPClient, s.Scheme) + } else { + s.serviceHandler = v2.NewServiceHandlerWithHTTPClient(s.BaseURL, s.HTTPClient) + } } diff --git a/pkg/api/utils/shipyardControllerUtils.go b/pkg/api/utils/shipyardControllerUtils.go index 70b09f62..001a15ca 100644 --- a/pkg/api/utils/shipyardControllerUtils.go +++ b/pkg/api/utils/shipyardControllerUtils.go @@ -1,44 +1,44 @@ package api import ( - "crypto/tls" - "io/ioutil" + "context" "net/http" - "net/url" - "strconv" "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" ) const shipyardControllerBaseURL = "controlPlane" type ShipyardControlV1Interface interface { + // GetOpenTriggeredEvents returns all open triggered events. GetOpenTriggeredEvents(filter EventFilter) ([]*models.KeptnContextExtendedCE, error) } // ShipyardControllerHandler handles services type ShipyardControllerHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + shipyardControllerHandler *v2.ShipyardControllerHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } // NewShipyardControllerHandler returns a new ShipyardControllerHandler which sends all requests directly to the configuration-service func NewShipyardControllerHandler(baseURL string) *ShipyardControllerHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewShipyardControllerHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewShipyardControllerHandlerWithHTTPClient returns a new ShipyardControllerHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewShipyardControllerHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ShipyardControllerHandler { return &ShipyardControllerHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + shipyardControllerHandler: v2.NewShipyardControllerHandlerWithHTTPClient(baseURL, httpClient), } } @@ -54,19 +54,20 @@ func NewAuthenticatedShipyardControllerHandler(baseURL string, authToken string, } func createAuthenticatedShipyardControllerHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ShipyardControllerHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") + v2ShipyardControllerHandler := v2.NewAuthenticatedShipyardControllerHandler(baseURL, authToken, authHeader, httpClient, scheme) baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } + return &ShipyardControllerHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + shipyardControllerHandler: v2ShipyardControllerHandler, } } @@ -86,75 +87,20 @@ func (s *ShipyardControllerHandler) getHTTPClient() *http.Client { return s.HTTPClient } -// GetOpenTriggeredEvents returns all open triggered events +// GetOpenTriggeredEvents returns all open triggered events. func (s *ShipyardControllerHandler) GetOpenTriggeredEvents(filter EventFilter) ([]*models.KeptnContextExtendedCE, error) { - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - - events := []*models.KeptnContextExtendedCE{} - nextPageKey := "" - - for { - url, err := url.Parse(s.Scheme + "://" + s.getBaseURL() + v1EventPath + "/triggered/" + filter.EventType) - - q := url.Query() - if nextPageKey != "" { - q.Set("nextPageKey", nextPageKey) - url.RawQuery = q.Encode() - } - if filter.Project != "" { - q.Set("project", filter.Project) - } - if filter.Service != "" { - q.Set("service", filter.Service) - } - if filter.Stage != "" { - q.Set("stage", filter.Stage) - } - - url.RawQuery = q.Encode() - - if err != nil { - return nil, err - } - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, s) - - resp, err := s.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - if resp.StatusCode == 200 { - received := &models.Events{} - if err = received.FromJSON(body); err != nil { - return nil, err - } - events = append(events, received.Events...) - - if received.NextPageKey == "" || received.NextPageKey == "0" { - break - } - - nextPageKeyInt, _ := strconv.Atoi(received.NextPageKey) - - if filter.NumberOfPages > 0 && nextPageKeyInt >= filter.NumberOfPages { - break - } - - nextPageKey = received.NextPageKey - } else { - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() - } + s.ensureHandlerIsSet() + return s.shipyardControllerHandler.GetOpenTriggeredEvents(context.TODO(), *toV2EventFilter(&filter), v2.ShipyardControlGetOpenTriggeredEventsOptions{}) +} + +func (s *ShipyardControllerHandler) ensureHandlerIsSet() { + if s.shipyardControllerHandler != nil { + return + } + + if s.AuthToken != "" { + s.shipyardControllerHandler = v2.NewAuthenticatedShipyardControllerHandler(s.BaseURL, s.AuthToken, s.AuthHeader, s.HTTPClient, s.Scheme) + } else { + s.shipyardControllerHandler = v2.NewShipyardControllerHandlerWithHTTPClient(s.BaseURL, s.HTTPClient) } - return events, nil } diff --git a/pkg/api/utils/stageUtils.go b/pkg/api/utils/stageUtils.go index 891c8a5a..404196b3 100644 --- a/pkg/api/utils/stageUtils.go +++ b/pkg/api/utils/stageUtils.go @@ -1,43 +1,46 @@ package api import ( - "crypto/tls" - "io/ioutil" + "context" "net/http" - "net/url" "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" + "github.com/keptn/go-utils/pkg/common/httputils" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) type StagesV1Interface interface { + // CreateStage creates a new stage with the provided name. CreateStage(project string, stageName string) (*models.EventContext, *models.Error) + + // GetAllStages returns a list of all stages. GetAllStages(project string) ([]*models.Stage, error) } // StageHandler handles stages type StageHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + stageHandler *v2.StageHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } // NewStageHandler returns a new StageHandler which sends all requests directly to the configuration-service func NewStageHandler(baseURL string) *StageHandler { - if strings.Contains(baseURL, "https://") { - baseURL = strings.TrimPrefix(baseURL, "https://") - } else if strings.Contains(baseURL, "http://") { - baseURL = strings.TrimPrefix(baseURL, "http://") - } + return NewStageHandlerWithHTTPClient(baseURL, &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}) +} + +// NewStageHandlerWithHTTPClient returns a new StageHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewStageHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *StageHandler { return &StageHandler{ - BaseURL: baseURL, - AuthHeader: "", - AuthToken: "", - HTTPClient: &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + stageHandler: v2.NewStageHandlerWithHTTPClient(baseURL, httpClient), } } @@ -53,19 +56,20 @@ func NewAuthenticatedStageHandler(baseURL string, authToken string, authHeader s } func createAuthenticatedStageHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *StageHandler { - baseURL = strings.TrimPrefix(baseURL, "http://") - baseURL = strings.TrimPrefix(baseURL, "https://") - baseURL = strings.TrimRight(baseURL, "/") + v2StageHandler := v2.NewAuthenticatedStageHandler(baseURL, authToken, authHeader, httpClient, scheme) + baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } + return &StageHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + stageHandler: v2StageHandler, } } @@ -85,63 +89,26 @@ func (s *StageHandler) getHTTPClient() *http.Client { return s.HTTPClient } -// CreateStage creates a new stage with the provided name +// CreateStage creates a new stage with the provided name. func (s *StageHandler) CreateStage(project string, stageName string) (*models.EventContext, *models.Error) { - - stage := models.Stage{StageName: stageName} - body, err := stage.ToJSON() - if err != nil { - return nil, buildErrorResponse(err.Error()) - } - return postWithEventContext(s.Scheme+"://"+s.BaseURL+v1ProjectPath+"/"+project+pathToStage, body, s) + s.ensureHandlerIsSet() + return s.stageHandler.CreateStage(context.TODO(), project, stageName, v2.StagesCreateStageOptions{}) } // GetAllStages returns a list of all stages. func (s *StageHandler) GetAllStages(project string) ([]*models.Stage, error) { + s.ensureHandlerIsSet() + return s.stageHandler.GetAllStages(context.TODO(), project, v2.StagesGetAllStagesOptions{}) +} + +func (s *StageHandler) ensureHandlerIsSet() { + if s.stageHandler != nil { + return + } - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - stages := []*models.Stage{} - - nextPageKey := "" - for { - url, err := url.Parse(s.Scheme + "://" + s.getBaseURL() + v1ProjectPath + "/" + project + pathToStage) - if err != nil { - return nil, err - } - q := url.Query() - if nextPageKey != "" { - q.Set("nextPageKey", nextPageKey) - url.RawQuery = q.Encode() - } - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, s) - - resp, err := s.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - body, _ := ioutil.ReadAll(resp.Body) - - if resp.StatusCode == 200 { - received := &models.Stages{} - if err = received.FromJSON(body); err != nil { - return nil, err - } - stages = append(stages, received.Stages...) - - if received.NextPageKey == "" || received.NextPageKey == "0" { - break - } - nextPageKey = received.NextPageKey - } else { - return nil, handleErrStatusCode(resp.StatusCode, body).ToError() - } + if s.AuthToken != "" { + s.stageHandler = v2.NewAuthenticatedStageHandler(s.BaseURL, s.AuthToken, s.AuthHeader, s.HTTPClient, s.Scheme) + } else { + s.stageHandler = v2.NewStageHandlerWithHTTPClient(s.BaseURL, s.HTTPClient) } - return stages, nil } diff --git a/pkg/api/utils/uniformUtils.go b/pkg/api/utils/uniformUtils.go index 970303f0..398e08b5 100644 --- a/pkg/api/utils/uniformUtils.go +++ b/pkg/api/utils/uniformUtils.go @@ -1,15 +1,12 @@ package api import ( - "encoding/json" - "errors" - "fmt" - "io/ioutil" + "context" "net/http" - "net/url" "strings" "github.com/keptn/go-utils/pkg/api/models" + v2 "github.com/keptn/go-utils/pkg/api/utils/v2" "github.com/keptn/go-utils/pkg/common/httputils" ) @@ -25,21 +22,26 @@ type UniformV1Interface interface { } type UniformHandler struct { - BaseURL string - AuthToken string - AuthHeader string - HTTPClient *http.Client - Scheme string + uniformHandler *v2.UniformHandler + BaseURL string + AuthToken string + AuthHeader string + HTTPClient *http.Client + Scheme string } +// NewUniformHandler returns a new UniformHandler func NewUniformHandler(baseURL string) *UniformHandler { - baseURL = httputils.TrimHTTPScheme(baseURL) + return NewUniformHandlerWithHTTPClient(baseURL, &http.Client{Transport: getClientTransport(nil)}) +} + +// NewUniformHandlerWithHTTPClient returns a new UniformHandler using the specified http.Client +func NewUniformHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *UniformHandler { return &UniformHandler{ - BaseURL: baseURL, - AuthToken: "", - AuthHeader: "", - HTTPClient: &http.Client{Transport: getClientTransport(nil)}, - Scheme: "http", + BaseURL: httputils.TrimHTTPScheme(baseURL), + HTTPClient: httpClient, + Scheme: "http", + uniformHandler: v2.NewUniformHandlerWithHTTPClient(baseURL, httpClient), } } @@ -54,19 +56,20 @@ func NewAuthenticatedUniformHandler(baseURL string, authToken string, authHeader } func createAuthenticatedUniformHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *UniformHandler { - baseURL = httputils.TrimHTTPScheme(baseURL) - baseURL = strings.TrimRight(baseURL, "/") + v2UniformHandler := v2.NewAuthenticatedUniformHandler(baseURL, authToken, authHeader, httpClient, scheme) + baseURL = strings.TrimRight(baseURL, "/") if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { baseURL += "/" + shipyardControllerBaseURL } return &UniformHandler{ - BaseURL: baseURL, - AuthHeader: authHeader, - AuthToken: authToken, - HTTPClient: httpClient, - Scheme: scheme, + BaseURL: httputils.TrimHTTPScheme(baseURL), + AuthHeader: authHeader, + AuthToken: authToken, + HTTPClient: httpClient, + Scheme: scheme, + uniformHandler: v2UniformHandler, } } @@ -87,101 +90,38 @@ func (u *UniformHandler) getHTTPClient() *http.Client { } func (u *UniformHandler) Ping(integrationID string) (*models.Integration, error) { - if integrationID == "" { - return nil, errors.New("could not ping an invalid IntegrationID") - } - - resp, err := put(u.Scheme+"://"+u.getBaseURL()+v1UniformPath+"/"+integrationID+"/ping", nil, u) - if err != nil { - return nil, errors.New(err.GetMessage()) - } - - response := &models.Integration{} - if err := response.FromJSON([]byte(resp)); err != nil { - return nil, err - } - - return response, nil + u.ensureHandlerIsSet() + return u.uniformHandler.Ping(context.TODO(), integrationID, v2.UniformPingOptions{}) } func (u *UniformHandler) RegisterIntegration(integration models.Integration) (string, error) { - bodyStr, err := integration.ToJSON() - if err != nil { - return "", err - } - - resp, errResponse := post(u.Scheme+"://"+u.getBaseURL()+v1UniformPath, bodyStr, u) - if errResponse != nil { - return "", fmt.Errorf(errResponse.GetMessage()) - } - - registerIntegrationResponse := &models.RegisterIntegrationResponse{} - if err := registerIntegrationResponse.FromJSON([]byte(resp)); err != nil { - return "", err - } - - return registerIntegrationResponse.ID, nil + u.ensureHandlerIsSet() + return u.uniformHandler.RegisterIntegration(context.TODO(), integration, v2.UniformRegisterIntegrationOptions{}) } func (u *UniformHandler) CreateSubscription(integrationID string, subscription models.EventSubscription) (string, error) { - bodyStr, err := subscription.ToJSON() - if err != nil { - return "", err - } - resp, errResponse := post(u.Scheme+"://"+u.getBaseURL()+v1UniformPath+"/"+integrationID+"/subscription", bodyStr, u) - if errResponse != nil { - return "", fmt.Errorf(errResponse.GetMessage()) - } - _ = resp - - createSubscriptionResponse := &models.CreateSubscriptionResponse{} - if err := createSubscriptionResponse.FromJSON([]byte(resp)); err != nil { - return "", err - } - - return createSubscriptionResponse.ID, nil + u.ensureHandlerIsSet() + return u.uniformHandler.CreateSubscription(context.TODO(), integrationID, subscription, v2.UniformCreateSubscriptionOptions{}) } func (u *UniformHandler) UnregisterIntegration(integrationID string) error { - _, err := delete(u.Scheme+"://"+u.getBaseURL()+v1UniformPath+"/"+integrationID, u) - if err != nil { - return fmt.Errorf(err.GetMessage()) - } - return nil + u.ensureHandlerIsSet() + return u.uniformHandler.UnregisterIntegration(context.TODO(), integrationID, v2.UniformUnregisterIntegrationOptions{}) } func (u *UniformHandler) GetRegistrations() ([]*models.Integration, error) { - url, err := url.Parse(u.Scheme + "://" + u.getBaseURL() + v1UniformPath) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", url.String(), nil) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - addAuthHeader(req, u) - - resp, err := u.HTTPClient.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() + u.ensureHandlerIsSet() + return u.uniformHandler.GetRegistrations(context.TODO(), v2.UniformGetRegistrationsOptions{}) +} - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, err +func (u *UniformHandler) ensureHandlerIsSet() { + if u.uniformHandler != nil { + return } - if resp.StatusCode == http.StatusOK { - var received []*models.Integration - err := json.Unmarshal(body, &received) - if err != nil { - return nil, err - } - return received, nil + if u.AuthToken != "" { + u.uniformHandler = v2.NewAuthenticatedUniformHandler(u.BaseURL, u.AuthToken, u.AuthHeader, u.HTTPClient, u.Scheme) + } else { + u.uniformHandler = v2.NewUniformHandlerWithHTTPClient(u.BaseURL, u.HTTPClient) } - - return nil, nil } diff --git a/pkg/api/utils/v2/apiServiceUtils.go b/pkg/api/utils/v2/apiServiceUtils.go new file mode 100644 index 00000000..33abfecb --- /dev/null +++ b/pkg/api/utils/v2/apiServiceUtils.go @@ -0,0 +1,340 @@ +package v2 + +import ( + "bytes" + "context" + "crypto/tls" + "fmt" + "io/ioutil" + "net/http" + + "github.com/keptn/go-utils/pkg/api/models" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +// APIService represents the interface for accessing the configuration service +type APIService interface { + getBaseURL() string + getAuthToken() string + getAuthHeader() string + getHTTPClient() *http.Client +} + +// createInstrumentedClientTransport tries to add support for opentelemetry +// to the given http.Client. If httpClient is nil, a fresh http.Client +// with opentelemetry support is created +func createInstrumentedClientTransport(httpClient *http.Client) *http.Client { + if httpClient == nil { + return &http.Client{ + Transport: wrapOtelTransport(getClientTransport(nil)), + } + } + httpClient.Transport = wrapOtelTransport(getClientTransport(httpClient.Transport)) + return httpClient +} + +// Wraps the provided http.RoundTripper with one that +// starts a span and injects the span context into the outbound request headers. +func wrapOtelTransport(base http.RoundTripper) *otelhttp.Transport { + return otelhttp.NewTransport(base) +} + +// getClientTransport returns a client transport which +// skips verifying server certificates and is able to +// read proxy configuration from environment variables +// +// If the given http.RoundTripper is nil then a new http.Transport +// is created, otherwise the given http.RoundTripper is analysed whether it +// is of type *http.Transport. If so, the respective settings for +// disabling server certificate verification as well as proxy server support are set +// If not, the given http.RoundTripper is passed through untouched +func getClientTransport(rt http.RoundTripper) http.RoundTripper { + if rt == nil { + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + Proxy: http.ProxyFromEnvironment, + } + return tr + } + if tr, isDefaultTransport := rt.(*http.Transport); isDefaultTransport { + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + tr.Proxy = http.ProxyFromEnvironment + return tr + } + return rt +} + +func getAndExpectOK(ctx context.Context, uri string, api APIService) ([]byte, *models.Error) { + body, statusCode, status, err := get(ctx, uri, api) + if err != nil { + return nil, err + } + + if statusCode == 200 { + return body, nil + } + + if len(body) > 0 { + return nil, handleErrStatusCode(statusCode, body) + } + + return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", statusCode, status)) +} + +func getAndExpectSuccess(ctx context.Context, uri string, api APIService) ([]byte, *models.Error) { + body, statusCode, status, err := get(ctx, uri, api) + if err != nil { + return nil, err + } + + if statusCode >= 200 && statusCode < 300 { + return body, nil + } + + if len(body) > 0 { + return nil, handleErrStatusCode(statusCode, body) + } + + return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", statusCode, status)) +} + +func get(ctx context.Context, uri string, api APIService) ([]byte, int, string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "GET", uri, nil) + if err != nil { + return nil, 0, "", buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return nil, 0, "", buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, 0, "", buildErrorResponse(err.Error()) + } + + return body, resp.StatusCode, resp.Status, nil +} + +func putWithEventContext(ctx context.Context, uri string, data []byte, api APIService) (*models.EventContext, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "PUT", uri, bytes.NewBuffer(data)) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + + if resp.StatusCode >= 200 && resp.StatusCode <= 204 { + if len(body) == 0 { + return nil, nil + } + + eventContext := &models.EventContext{} + + if err = eventContext.FromJSON(body); err != nil { + // failed to parse json + return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) + } + + if eventContext.KeptnContext != nil { + fmt.Println("ID of Keptn context: " + *eventContext.KeptnContext) + } + return eventContext, nil + } + + if len(body) > 0 { + return nil, handleErrStatusCode(resp.StatusCode, body) + } + + return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) +} + +func put(ctx context.Context, uri string, data []byte, api APIService) (string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "PUT", uri, bytes.NewBuffer(data)) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + + if resp.StatusCode >= 200 && resp.StatusCode <= 204 { + return string(body), nil + } + + if len(body) > 0 { + return "", handleErrStatusCode(resp.StatusCode, body) + } + + return "", buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) +} + +func postWithEventContext(ctx context.Context, uri string, data []byte, api APIService) (*models.EventContext, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(data)) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + + if resp.StatusCode >= 200 && resp.StatusCode <= 204 { + if len(body) == 0 { + return nil, nil + } + + eventContext := &models.EventContext{} + if err = eventContext.FromJSON(body); err != nil { + // failed to parse json + return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) + } + + if eventContext.KeptnContext != nil { + fmt.Println("ID of Keptn context: " + *eventContext.KeptnContext) + } + return eventContext, nil + } + + if len(body) > 0 { + return nil, handleErrStatusCode(resp.StatusCode, body) + } + + return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) +} + +func post(ctx context.Context, uri string, data []byte, api APIService) (string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "POST", uri, bytes.NewBuffer(data)) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + + if resp.StatusCode >= 200 && resp.StatusCode <= 204 { + return string(body), nil + } + + if len(body) > 0 { + return "", handleErrStatusCode(resp.StatusCode, body) + } + + return "", buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", resp.StatusCode, resp.Status)) +} + +func deleteWithEventContext(ctx context.Context, uri string, api APIService) (*models.EventContext, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "DELETE", uri, nil) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + if len(body) == 0 { + return nil, nil + } + + eventContext := &models.EventContext{} + if err = eventContext.FromJSON(body); err != nil { + // failed to parse json + return nil, buildErrorResponse(err.Error() + "\n" + "-----DETAILS-----" + string(body)) + } + return eventContext, nil + } + + return nil, handleErrStatusCode(resp.StatusCode, body) +} + +func delete(ctx context.Context, uri string, api APIService) (string, *models.Error) { + req, err := http.NewRequestWithContext(ctx, "DELETE", uri, nil) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, api) + + resp, err := api.getHTTPClient().Do(req) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", buildErrorResponse(err.Error()) + } + + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return string(body), nil + } + + return "", handleErrStatusCode(resp.StatusCode, body) +} + +func buildErrorResponse(errorStr string) *models.Error { + err := models.Error{Message: &errorStr} + return &err +} + +func addAuthHeader(req *http.Request, api APIService) { + if api.getAuthHeader() != "" && api.getAuthToken() != "" { + req.Header.Set(api.getAuthHeader(), api.getAuthToken()) + } +} diff --git a/pkg/api/utils/v2/apiServiceUtils_test.go b/pkg/api/utils/v2/apiServiceUtils_test.go new file mode 100644 index 00000000..ad5f9021 --- /dev/null +++ b/pkg/api/utils/v2/apiServiceUtils_test.go @@ -0,0 +1,27 @@ +package v2 + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +func Test_createInstrumentedClientTransport(t *testing.T) { + client := createInstrumentedClientTransport(nil) + assert.NotNil(t, client) + assert.NotNil(t, client) + _, isOtelTransport := client.Transport.(*otelhttp.Transport) + assert.True(t, isOtelTransport) + + client = createInstrumentedClientTransport(&http.Client{}) + assert.NotNil(t, client) + _, isOtelTransport = client.Transport.(*otelhttp.Transport) + assert.True(t, isOtelTransport) + + client = createInstrumentedClientTransport(&http.Client{Transport: &http.Transport{}}) + assert.NotNil(t, client) + _, isOtelTransport = client.Transport.(*otelhttp.Transport) + assert.True(t, isOtelTransport) +} diff --git a/pkg/api/utils/v2/apiUtils.go b/pkg/api/utils/v2/apiUtils.go new file mode 100644 index 00000000..97fb27aa --- /dev/null +++ b/pkg/api/utils/v2/apiUtils.go @@ -0,0 +1,226 @@ +package v2 + +import ( + "context" + "net/http" + "strings" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +const v1EventPath = "/v1/event" +const v1MetadataPath = "/v1/metadata" + +// APISendEventOptions are options for APIInterface.SendEvent(). +type APISendEventOptions struct{} + +// APITriggerEvaluationOptions are options for APIInterface.TriggerEvaluation(). +type APITriggerEvaluationOptions struct{} + +// APICreateProjectOptions are options for APIInterface.CreateProject(). +type APICreateProjectOptions struct{} + +// APIUpdateProjectOptions are options for APIInterface.UpdateProject(). +type APIUpdateProjectOptions struct{} + +// APIDeleteProjectOptions are options for APIInterface.DeleteProject(). +type APIDeleteProjectOptions struct{} + +// APICreateServiceOptions are options for APIInterface.CreateService(). +type APICreateServiceOptions struct{} + +// APIDeleteServiceOptions are options for APIInterface.DeleteService(). +type APIDeleteServiceOptions struct{} + +// APIGetMetadataOptions are options for APIInterface.GetMetadata(). +type APIGetMetadataOptions struct{} + +type APIInterface interface { + // SendEvent sends an event to Keptn. + SendEvent(ctx context.Context, event models.KeptnContextExtendedCE, opts APISendEventOptions) (*models.EventContext, *models.Error) + + // TriggerEvaluation triggers a new evaluation. + TriggerEvaluation(ctx context.Context, project string, stage string, service string, evaluation models.Evaluation, opts APITriggerEvaluationOptions) (*models.EventContext, *models.Error) + + // CreateProject creates a new project. + CreateProject(ctx context.Context, project models.CreateProject, opts APICreateProjectOptions) (string, *models.Error) + + // UpdateProject updates a project. + UpdateProject(ctx context.Context, project models.CreateProject, opts APIUpdateProjectOptions) (string, *models.Error) + + // DeleteProject deletes a project. + DeleteProject(ctx context.Context, project models.Project, opts APIDeleteProjectOptions) (*models.DeleteProjectResponse, *models.Error) + + // CreateService creates a new service. + CreateService(ctx context.Context, project string, service models.CreateService, opts APICreateServiceOptions) (string, *models.Error) + + // DeleteService deletes a service. + DeleteService(ctx context.Context, project string, service string, opts APIDeleteServiceOptions) (*models.DeleteServiceResponse, *models.Error) + + // GetMetadata retrieves Keptn metadata information. + GetMetadata(ctx context.Context, opts APIGetMetadataOptions) (*models.Metadata, *models.Error) +} + +type APIHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewAPIHandler returns a new APIHandler +func NewAPIHandler(baseURL string) *APIHandler { + return NewAPIHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewAPIHandlerWithHTTPClient returns a new APIHandler using the specified http.Client +func NewAPIHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *APIHandler { + return createAPIHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedAPIHandler returns a new APIHandler that authenticates at the api-service endpoint via the provided token +func NewAuthenticatedAPIHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *APIHandler { + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createAPIHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createAPIHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *APIHandler { + return &APIHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (a *APIHandler) getBaseURL() string { + return a.baseURL +} + +func (a *APIHandler) getAuthToken() string { + return a.authToken +} + +func (a *APIHandler) getAuthHeader() string { + return a.authHeader +} + +func (a *APIHandler) getHTTPClient() *http.Client { + return a.httpClient +} + +// SendEvent sends an event to Keptn. +func (a *APIHandler) SendEvent(ctx context.Context, event models.KeptnContextExtendedCE, opts APISendEventOptions) (*models.EventContext, *models.Error) { + baseURL := a.getAPIServicePath() + + bodyStr, err := event.ToJSON() + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + + return postWithEventContext(ctx, a.scheme+"://"+baseURL+v1EventPath, bodyStr, a) +} + +// TriggerEvaluation triggers a new evaluation. +func (a *APIHandler) TriggerEvaluation(ctx context.Context, project, stage, service string, evaluation models.Evaluation, opts APITriggerEvaluationOptions) (*models.EventContext, *models.Error) { + bodyStr, err := evaluation.ToJSON() + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + return postWithEventContext(ctx, a.scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+service+"/evaluation", bodyStr, a) +} + +// CreateProject creates a new project. +func (a *APIHandler) CreateProject(ctx context.Context, project models.CreateProject, opts APICreateProjectOptions) (string, *models.Error) { + + bodyStr, err := project.ToJSON() + if err != nil { + return "", buildErrorResponse(err.Error()) + } + return post(ctx, a.scheme+"://"+a.getBaseURL()+v1ProjectPath, bodyStr, a) +} + +// UpdateProject updates a project. +func (a *APIHandler) UpdateProject(ctx context.Context, project models.CreateProject, opts APIUpdateProjectOptions) (string, *models.Error) { + bodyStr, err := project.ToJSON() + if err != nil { + return "", buildErrorResponse(err.Error()) + } + return put(ctx, a.scheme+"://"+a.getBaseURL()+v1ProjectPath, bodyStr, a) +} + +// DeleteProject deletes a project. +func (a *APIHandler) DeleteProject(ctx context.Context, project models.Project, opts APIDeleteProjectOptions) (*models.DeleteProjectResponse, *models.Error) { + resp, err := delete(ctx, a.scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, a) + if err != nil { + return nil, err + } + + deletePrjResponse := &models.DeleteProjectResponse{} + if err2 := deletePrjResponse.FromJSON([]byte(resp)); err2 != nil { + msg := "Could not decode DeleteProjectResponse: " + err2.Error() + return nil, &models.Error{ + Message: &msg, + } + } + return deletePrjResponse, nil +} + +// CreateService creates a new service. +func (a *APIHandler) CreateService(ctx context.Context, project string, service models.CreateService, opts APICreateServiceOptions) (string, *models.Error) { + bodyStr, err := service.ToJSON() + if err != nil { + return "", buildErrorResponse(err.Error()) + } + return post(ctx, a.scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project+pathToService, bodyStr, a) +} + +// DeleteService deletes a service. +func (a *APIHandler) DeleteService(ctx context.Context, project, service string, opts APIDeleteServiceOptions) (*models.DeleteServiceResponse, *models.Error) { + resp, err := delete(ctx, a.scheme+"://"+a.getBaseURL()+v1ProjectPath+"/"+project+pathToService+"/"+service, a) + + if err != nil { + return nil, err + } + + deleteSvcResponse := &models.DeleteServiceResponse{} + if err2 := deleteSvcResponse.FromJSON([]byte(resp)); err2 != nil { + msg := "Could not decode DeleteServiceResponse: " + err2.Error() + return nil, &models.Error{ + Message: &msg, + } + } + return deleteSvcResponse, nil +} + +// GetMetadata retrieves Keptn metadata information. +func (a *APIHandler) GetMetadata(ctx context.Context, opts APIGetMetadataOptions) (*models.Metadata, *models.Error) { + baseURL := a.getAPIServicePath() + + body, mErr := getAndExpectSuccess(ctx, a.scheme+"://"+baseURL+v1MetadataPath, a) + if mErr != nil { + return nil, mErr + + } + + respMetadata := &models.Metadata{} + if err := respMetadata.FromJSON(body); err != nil { + return nil, buildErrorResponse(err.Error()) + } + + return respMetadata, nil +} + +func (a *APIHandler) getAPIServicePath() string { + baseURL := a.getBaseURL() + if strings.HasSuffix(baseURL, "/"+shipyardControllerBaseURL) { + baseURL = strings.TrimSuffix(a.getBaseURL(), "/"+shipyardControllerBaseURL) + } + return baseURL +} diff --git a/pkg/api/utils/apiUtils_test.go b/pkg/api/utils/v2/apiUtils_test.go similarity index 94% rename from pkg/api/utils/apiUtils_test.go rename to pkg/api/utils/v2/apiUtils_test.go index 242dd9f9..60a25aef 100644 --- a/pkg/api/utils/apiUtils_test.go +++ b/pkg/api/utils/v2/apiUtils_test.go @@ -1,8 +1,9 @@ -package api +package v2 import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestAPIHandler_getAPIServicePath(t *testing.T) { @@ -32,7 +33,7 @@ func TestAPIHandler_getAPIServicePath(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := &APIHandler{ - BaseURL: tt.fields.BaseURL, + baseURL: tt.fields.BaseURL, } assert.Equalf(t, tt.want, a.getAPIServicePath(), "getAPIServicePath()") }) diff --git a/pkg/api/utils/v2/authUtils.go b/pkg/api/utils/v2/authUtils.go new file mode 100644 index 00000000..035d4264 --- /dev/null +++ b/pkg/api/utils/v2/authUtils.go @@ -0,0 +1,71 @@ +package v2 + +import ( + "context" + "net/http" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +// AuthAuthenticateOptions are options for AuthInterface.Authenticate(). +type AuthAuthenticateOptions struct{} + +type AuthInterface interface { + // Authenticate authenticates the client request against the server. + Authenticate(ctx context.Context, opts AuthAuthenticateOptions) (*models.EventContext, *models.Error) +} + +type AuthHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewAuthHandler returns a new AuthHandler +func NewAuthHandler(baseURL string) *AuthHandler { + return NewAuthHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewAuthHandlerWithHTTPClient returns a new AuthHandler using the specified http.Client +func NewAuthHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *AuthHandler { + return createAuthHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedAuthHandler returns a new AuthHandler that authenticates at the endpoint via the provided token +func NewAuthenticatedAuthHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *AuthHandler { + return createAuthHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createAuthHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *AuthHandler { + return &AuthHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (a *AuthHandler) getBaseURL() string { + return a.baseURL +} + +func (a *AuthHandler) getAuthToken() string { + return a.authToken +} + +func (a *AuthHandler) getAuthHeader() string { + return a.authHeader +} + +func (a *AuthHandler) getHTTPClient() *http.Client { + return a.httpClient +} + +// Authenticate authenticates the client request against the server. +func (a *AuthHandler) Authenticate(ctx context.Context, opts AuthAuthenticateOptions) (*models.EventContext, *models.Error) { + return postWithEventContext(ctx, a.scheme+"://"+a.getBaseURL()+"/v1/auth", nil, a) +} diff --git a/pkg/api/utils/v2/client.go b/pkg/api/utils/v2/client.go new file mode 100644 index 00000000..a57592f6 --- /dev/null +++ b/pkg/api/utils/v2/client.go @@ -0,0 +1,181 @@ +package v2 + +import ( + "fmt" + "net/http" + "net/url" +) + +var _ KeptnInterface = (*APISet)(nil) + +type KeptnInterface interface { + API() APIInterface + Auth() AuthInterface + Events() EventsInterface + Logs() LogsInterface + Projects() ProjectsInterface + Resources() ResourcesInterface + Secrets() SecretsInterface + Sequences() SequencesInterface + Services() ServicesInterface + Stages() StagesInterface + Uniform() UniformInterface + ShipyardControl() ShipyardControlInterface +} + +// APISet contains the API utils for all Keptn APIs +type APISet struct { + endpointURL *url.URL + apiToken string + authHeader string + scheme string + httpClient *http.Client + apiHandler *APIHandler + authHandler *AuthHandler + eventHandler *EventHandler + logHandler *LogHandler + projectHandler *ProjectHandler + resourceHandler *ResourceHandler + secretHandler *SecretHandler + sequenceControlHandler *SequenceControlHandler + serviceHandler *ServiceHandler + stageHandler *StageHandler + uniformHandler *UniformHandler + shipyardControlHandler *ShipyardControllerHandler +} + +// API retrieves the APIHandler +func (c *APISet) API() APIInterface { + return c.apiHandler +} + +// Auth retrieves the AuthHandler +func (c *APISet) Auth() AuthInterface { + return c.authHandler +} + +// Events retrieves the EventHandler +func (c *APISet) Events() EventsInterface { + return c.eventHandler +} + +// Logs retrieves the LogHandler +func (c *APISet) Logs() LogsInterface { + return c.logHandler +} + +// Projects retrieves the ProjectHandler +func (c *APISet) Projects() ProjectsInterface { + return c.projectHandler +} + +// Resources retrieves the ResourceHandler +func (c *APISet) Resources() ResourcesInterface { + return c.resourceHandler +} + +// Secrets retrieves the SecretHandler +func (c *APISet) Secrets() SecretsInterface { + return c.secretHandler +} + +// Sequences retrieves the SequenceControlHandler +func (c *APISet) Sequences() SequencesInterface { + return c.sequenceControlHandler +} + +// Services retrieves the ServiceHandler +func (c *APISet) Services() ServicesInterface { + return c.serviceHandler +} + +// Stages retrieves the StageHandler +func (c *APISet) Stages() StagesInterface { + return c.stageHandler +} + +// Uniform retrieves the UniformHandler +func (c *APISet) Uniform() UniformInterface { + return c.uniformHandler +} + +// ShipyardControl retrieves the ShipyardControllerHandler +func (c *APISet) ShipyardControl() ShipyardControlInterface { + return c.shipyardControlHandler +} + +// Token retrieves the API token +func (c *APISet) Token() string { + return c.apiToken +} + +// Endpoint retrieves the base API endpoint URL +func (c *APISet) Endpoint() *url.URL { + return c.endpointURL +} + +// WithAuthToken sets the given auth token. +// Optionally a custom auth header can be set (default x-token) +func WithAuthToken(authToken string, authHeader ...string) func(*APISet) { + aHeader := "x-token" + if len(authHeader) > 0 { + aHeader = authHeader[0] + } + return func(a *APISet) { + a.apiToken = authToken + a.authHeader = aHeader + } +} + +// WithHTTPClient configures a custom http client to use +func WithHTTPClient(client *http.Client) func(*APISet) { + return func(a *APISet) { + a.httpClient = client + } +} + +// WithScheme sets the scheme +// If this option is not used, then default scheme "http" is used by the APISet +func WithScheme(scheme string) func(*APISet) { + return func(a *APISet) { + a.scheme = scheme + } +} + +// New creates a new APISet instance +func New(baseURL string, options ...func(*APISet)) (*APISet, error) { + u, err := url.Parse(baseURL) + if err != nil { + return nil, fmt.Errorf("unable to create apiset: %w", err) + } + as := &APISet{} + for _, o := range options { + if o != nil { + o(as) + } + } + as.endpointURL = u + as.httpClient = createInstrumentedClientTransport(as.httpClient) + + if as.scheme == "" { + if as.endpointURL.Scheme != "" { + as.scheme = u.Scheme + } else { + as.scheme = "http" + } + } + + as.apiHandler = NewAuthenticatedAPIHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.authHandler = NewAuthenticatedAuthHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.logHandler = NewAuthenticatedLogHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.eventHandler = NewAuthenticatedEventHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.projectHandler = NewAuthenticatedProjectHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.resourceHandler = NewAuthenticatedResourceHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.secretHandler = NewAuthenticatedSecretHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.sequenceControlHandler = NewAuthenticatedSequenceControlHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.serviceHandler = NewAuthenticatedServiceHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.shipyardControlHandler = NewAuthenticatedShipyardControllerHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.stageHandler = NewAuthenticatedStageHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + as.uniformHandler = NewAuthenticatedUniformHandler(baseURL, as.apiToken, as.authHeader, as.httpClient, as.scheme) + return as, nil +} diff --git a/pkg/api/utils/v2/client_test.go b/pkg/api/utils/v2/client_test.go new file mode 100644 index 00000000..0bb85ecf --- /dev/null +++ b/pkg/api/utils/v2/client_test.go @@ -0,0 +1,61 @@ +package v2 + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestApiSetWithInvalidURL(t *testing.T) { + apiSet, err := New("://http.lol") + assert.Nil(t, apiSet) + assert.Error(t, err) +} + +func TestApiSetCreatesHandlers(t *testing.T) { + apiSet, err := New("http://base-url.com") + assert.NoError(t, err) + assert.Equal(t, "http://base-url.com", apiSet.Endpoint().String()) + assert.Equal(t, "http", apiSet.scheme) + assert.NotNil(t, apiSet.Uniform()) + assert.NotNil(t, apiSet.Endpoint()) + assert.NotNil(t, apiSet.ShipyardControl()) + assert.NotNil(t, apiSet.Stages()) + assert.NotNil(t, apiSet.Services()) + assert.NotNil(t, apiSet.Sequences()) + assert.NotNil(t, apiSet.Secrets()) + assert.NotNil(t, apiSet.Projects()) + assert.NotNil(t, apiSet.API()) + assert.NotNil(t, apiSet.Events()) + assert.NotNil(t, apiSet.Auth()) + assert.NotNil(t, apiSet.Resources()) + assert.NotNil(t, apiSet.Logs()) +} + +func TestAPISetDefaultValues(t *testing.T) { + apiSet, err := New("base-url.com") + assert.Nil(t, err) + assert.NotNil(t, apiSet) + assert.Equal(t, "http", apiSet.scheme) + assert.Equal(t, "", apiSet.authHeader) + assert.Equal(t, "", apiSet.apiToken) + assert.NotNil(t, apiSet.httpClient) + + apiSet, err = New("https://base-url.com") + assert.Nil(t, err) + assert.NotNil(t, apiSet) + assert.Equal(t, "https", apiSet.scheme) + assert.Equal(t, "", apiSet.authHeader) + assert.Equal(t, "", apiSet.apiToken) + assert.NotNil(t, apiSet.httpClient) +} + +func TestAPISetWithOptions(t *testing.T) { + apiSet, err := New("base-url.com", WithAuthToken("a-token"), WithHTTPClient(&http.Client{}), WithScheme("https")) + assert.NoError(t, err) + assert.Equal(t, "a-token", apiSet.Token()) + assert.Equal(t, "x-token", apiSet.authHeader) + assert.Equal(t, "https", apiSet.scheme) + assert.NotNil(t, apiSet.httpClient) +} diff --git a/pkg/api/utils/v2/errors.go b/pkg/api/utils/v2/errors.go new file mode 100644 index 00000000..d2f8d929 --- /dev/null +++ b/pkg/api/utils/v2/errors.go @@ -0,0 +1,19 @@ +package v2 + +import ( + "fmt" + + "github.com/keptn/go-utils/pkg/api/models" +) + +// ErrWithStatusCode message +const ErrWithStatusCode = "error with status code %d" + +func handleErrStatusCode(statusCode int, body []byte) *models.Error { + respErr := &models.Error{} + if err := respErr.FromJSON(body); err == nil && respErr != nil { + return respErr + } + + return buildErrorResponse(fmt.Sprintf(ErrWithStatusCode, statusCode)) +} diff --git a/pkg/api/utils/v2/eventUtils.go b/pkg/api/utils/v2/eventUtils.go new file mode 100644 index 00000000..9768f0e1 --- /dev/null +++ b/pkg/api/utils/v2/eventUtils.go @@ -0,0 +1,192 @@ +package v2 + +import ( + "context" + "fmt" + "log" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +// EventsGetEventsOptions are options for EventsInterface.GetEvents(). +type EventsGetEventsOptions struct{} + +// EventsGetEventsWithRetryOptions are options for EventsInterface.GetEventsWithRetry(). +type EventsGetEventsWithRetryOptions struct{} + +type EventsInterface interface { + // GetEvents returns all events matching the properties in the passed filter object. + GetEvents(ctx context.Context, filter *EventFilter, opts EventsGetEventsOptions) ([]*models.KeptnContextExtendedCE, *models.Error) + + // GetEventsWithRetry tries to retrieve events matching the passed filter. + GetEventsWithRetry(ctx context.Context, filter *EventFilter, maxRetries int, retrySleepTime time.Duration, opts EventsGetEventsWithRetryOptions) ([]*models.KeptnContextExtendedCE, error) +} + +type EventHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// EventFilter allows to filter events based on the provided properties +type EventFilter struct { + Project string + Stage string + Service string + EventType string + KeptnContext string + EventID string + PageSize string + NumberOfPages int + FromTime string +} + +// NewEventHandler returns a new EventHandler +func NewEventHandler(baseURL string) *EventHandler { + return NewEventHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewEventHandlerWithHTTPClient returns a new EventHandler using the specified http.Client +func NewEventHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *EventHandler { + return createEventHandler(baseURL, "", "", httpClient, "http") +} + +const mongodbDatastoreServiceBaseUrl = "mongodb-datastore" + +// NewAuthenticatedEventHandler returns a new EventHandler that authenticates at the endpoint via the provided token +func NewAuthenticatedEventHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *EventHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, mongodbDatastoreServiceBaseUrl) { + baseURL += "/" + mongodbDatastoreServiceBaseUrl + } + + return createEventHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createEventHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *EventHandler { + return &EventHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (e *EventHandler) getBaseURL() string { + return e.baseURL +} + +func (e *EventHandler) getAuthToken() string { + return e.authToken +} + +func (e *EventHandler) getAuthHeader() string { + return e.authHeader +} + +func (e *EventHandler) getHTTPClient() *http.Client { + return e.httpClient +} + +// GetEvents returns all events matching the properties in the passed filter object. +func (e *EventHandler) GetEvents(ctx context.Context, filter *EventFilter, opts EventsGetEventsOptions) ([]*models.KeptnContextExtendedCE, *models.Error) { + u, err := url.Parse(e.scheme + "://" + e.getBaseURL() + "/event?") + if err != nil { + log.Fatal("error parsing url") + } + + query := u.Query() + + if filter.Project != "" { + query.Set("project", filter.Project) + } + if filter.Stage != "" { + query.Set("stage", filter.Stage) + } + if filter.Service != "" { + query.Set("service", filter.Service) + } + if filter.KeptnContext != "" { + query.Set("keptnContext", filter.KeptnContext) + } + if filter.EventID != "" { + query.Set("eventID", filter.EventID) + } + if filter.EventType != "" { + query.Set("type", filter.EventType) + } + if filter.PageSize != "" { + query.Set("pageSize", filter.PageSize) + } + if filter.FromTime != "" { + query.Set("fromTime", filter.FromTime) + } + + u.RawQuery = query.Encode() + + return e.getEvents(ctx, u.String(), filter.NumberOfPages) +} + +// GetEventsWithRetry tries to retrieve events matching the passed filter. +func (e *EventHandler) GetEventsWithRetry(ctx context.Context, filter *EventFilter, maxRetries int, retrySleepTime time.Duration, opts EventsGetEventsWithRetryOptions) ([]*models.KeptnContextExtendedCE, error) { + for i := 0; i < maxRetries; i = i + 1 { + events, errObj := e.GetEvents(ctx, filter, EventsGetEventsOptions{}) + if errObj == nil && len(events) > 0 { + return events, nil + } + <-time.After(retrySleepTime) + } + return nil, fmt.Errorf("could not find matching event after %d x %s", maxRetries, retrySleepTime.String()) +} + +func (e *EventHandler) getEvents(ctx context.Context, uri string, numberOfPages int) ([]*models.KeptnContextExtendedCE, *models.Error) { + events := []*models.KeptnContextExtendedCE{} + nextPageKey := "" + + for { + url, err := url.Parse(uri) + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + q := url.Query() + if nextPageKey != "" { + q.Set("nextPageKey", nextPageKey) + url.RawQuery = q.Encode() + } + + body, mErr := getAndExpectOK(ctx, url.String(), e) + if mErr != nil { + return nil, mErr + } + + received := &models.Events{} + if err = received.FromJSON(body); err != nil { + return nil, buildErrorResponse(err.Error()) + } + + events = append(events, received.Events...) + + if received.NextPageKey == "" || received.NextPageKey == "0" { + break + } + + nextPageKeyInt, _ := strconv.Atoi(received.NextPageKey) + + if numberOfPages > 0 && nextPageKeyInt >= numberOfPages { + break + } + + nextPageKey = received.NextPageKey + } + + return events, nil +} diff --git a/pkg/api/utils/v2/eventwatch.go b/pkg/api/utils/v2/eventwatch.go new file mode 100644 index 00000000..16f0cb07 --- /dev/null +++ b/pkg/api/utils/v2/eventwatch.go @@ -0,0 +1,137 @@ +package v2 + +import ( + "context" + "log" + "sort" + "time" + + "github.com/keptn/go-utils/pkg/api/models" +) + +// EventWatcher implements the logic to query for events and provide them to the client +type EventWatcher struct { + nextCEFetchTime time.Time + eventHandler EventHandlerInterface + eventFilter EventFilter + ticker *time.Ticker + timeout <-chan time.Time +} + +// Watch starts the watch loop and returns a channel to get the actual events as well as a context.CancelFunc in order +// to stop the watch routine +func (ew *EventWatcher) Watch(ctx context.Context) (<-chan []*models.KeptnContextExtendedCE, context.CancelFunc) { + ctx, cancel := context.WithCancel(ctx) + ch := make(chan []*models.KeptnContextExtendedCE) + go ew.fetch(ctx, cancel, ch, ew.eventFilter) + return ch, cancel +} + +func (ew *EventWatcher) fetch(ctx context.Context, cancel context.CancelFunc, ch chan<- []*models.KeptnContextExtendedCE, filter EventFilter) { + defer func() { + cancel() + ew.ticker.Stop() + }() + + for { + // We need to query immediately because a time.Ticker cannot be configured + // to emmit a tick event immediately + ch <- ew.queryEvents(filter) + select { + // Query again once we receive a next tick + case <-ew.ticker.C: + continue + // Close the channel and break out once we reach a timeout + case <-ew.timeout: + close(ch) + return + // Close the channel and break out once the user cancels via the context + case <-ctx.Done(): + close(ch) + return + } + } +} + +func (ew *EventWatcher) queryEvents(filter EventFilter) []*models.KeptnContextExtendedCE { + + filter.FromTime = ew.nextCEFetchTime.Format("2006-01-02T15:04:05.000Z") + events, err := ew.eventHandler.GetEvents(&filter) + if err != nil { + log.Printf("Unable to fetch events: %s", *err.Message) + } + SortByTime(events) + if len(events) > 0 { + if events[len(events)-1].Time.After(ew.nextCEFetchTime) { + ew.nextCEFetchTime = events[len(events)-1].Time + } + } + + return events +} + +// NewEventWatcher creates a new event watcher with the given options +func NewEventWatcher(eventHandler EventHandlerInterface, opts ...EventWatcherOption) *EventWatcher { + e := &EventWatcher{ + nextCEFetchTime: time.Now().UTC(), + eventHandler: eventHandler, + eventFilter: EventFilter{}, + ticker: time.NewTicker(10 * time.Second), + timeout: nil, + } + + for _, opt := range opts { + opt(e) + } + + return e +} + +// EventWatcherOption can be used to configure the EventWatcher +type EventWatcherOption func(*EventWatcher) + +// WithEventFilter configures the EventWatcher to use a filter +func WithEventFilter(filter EventFilter) EventWatcherOption { + return func(ew *EventWatcher) { + ew.eventFilter = filter + } +} + +// WithStartTime configures the EventWatcher to use a specific start timestamp for the first query +func WithStartTime(startTime time.Time) EventWatcherOption { + return func(ew *EventWatcher) { + ew.nextCEFetchTime = startTime + } +} + +// WithInterval configures the EventWatcher to use a custom delay between each query +// You can use this to overwrite the default which is 10 * time.Second +func WithInterval(ticker *time.Ticker) EventWatcherOption { + return func(ew *EventWatcher) { + ew.ticker = ticker + } +} + +// WithTimeout configures the EventWatcher to use a custom timeout specifying +// after which duration the watcher shall stop +func WithTimeout(duration time.Duration) EventWatcherOption { + return func(ew *EventWatcher) { + ew.timeout = time.After(duration) + } +} + +// EventHandlerInterface is the api to fetch events from the event store +type EventHandlerInterface interface { + GetEventsWithRetry(filter *EventFilter, maxRetries int, retrySleepTime time.Duration) ([]*models.KeptnContextExtendedCE, error) + GetEvents(filter *EventFilter) ([]*models.KeptnContextExtendedCE, *models.Error) +} + +// EventManipulatorFunc can be used to manipulate a slice of events +type EventManipulatorFunc func([]*models.KeptnContextExtendedCE) + +// SortByTime sorts the event slice by time (oldest to newest) +func SortByTime(events []*models.KeptnContextExtendedCE) { + sort.Slice(events, func(i, j int) bool { + return events[i].Time.Before(events[j].Time) + }) +} diff --git a/pkg/api/utils/v2/eventwatch_test.go b/pkg/api/utils/v2/eventwatch_test.go new file mode 100644 index 00000000..8dc2a209 --- /dev/null +++ b/pkg/api/utils/v2/eventwatch_test.go @@ -0,0 +1,136 @@ +package v2 + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/timeutils" + "github.com/stretchr/testify/assert" +) + +type fakeEventHandler struct { + data map[string][]*models.KeptnContextExtendedCE +} + +func (fh *fakeEventHandler) GetEvents(filter *EventFilter) ([]*models.KeptnContextExtendedCE, *models.Error) { + events := fh.data[filter.KeptnContext] + fh.data = map[string][]*models.KeptnContextExtendedCE{} + return events, nil +} + +func (fh *fakeEventHandler) GetEventsWithRetry(filter *EventFilter, maxRetries int, retrySleepTime time.Duration) ([]*models.KeptnContextExtendedCE, error) { + panic("not implemented") +} + +func newFakeEventHandler() *fakeEventHandler { + return &fakeEventHandler{ + data: map[string][]*models.KeptnContextExtendedCE{ + "ctx1": { + { + ID: "ID1", + Shkeptncontext: "ctx1", + Time: t0.Add(time.Second), + }, + { + ID: "ID2", + Shkeptncontext: "ctx1", + Time: t0.Add(time.Second * 2), + }, + { + ID: "ID3", + Shkeptncontext: "ctx1", + Time: t0.Add(time.Second * 3), + }, + }, + "ctx2": { + { + ID: "ID1", + Shkeptncontext: "ctx2", + Time: t0.Add(time.Second * 30), + }, + { + ID: "ID2", + Shkeptncontext: "ctx2", + Time: t0.Add(time.Second * 31), + }, + }, + }, + } +} + +var t0 = time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC) + +func TestEventWatcher(t *testing.T) { + watcher := NewEventWatcher(newFakeEventHandler(), + WithEventFilter(EventFilter{KeptnContext: "ctx1"}), + WithInterval(time.NewTicker(1)), + ) + + stream, _ := watcher.Watch(context.Background()) + events, ok := <-stream + if !ok { + t.Fatalf("unexpected closed channel") + } + assert.Equal(t, 3, len(events)) +} + +func TestEventWatcherCancel(t *testing.T) { + watcher := NewEventWatcher(newFakeEventHandler(), + WithEventFilter(EventFilter{KeptnContext: "ctx1"}), + WithInterval(time.NewTicker(1)), + ) + + stream, cancel := watcher.Watch(context.Background()) + cancel() + for ev := range stream { + fmt.Println(ev) + } + + _, ok := <-stream + if ok { + t.Fatalf("unexpected opened channel") + } +} + +func TestEventWatcherTimeout(t *testing.T) { + watcher := NewEventWatcher(newFakeEventHandler(), + WithEventFilter(EventFilter{KeptnContext: "ctx1"}), + WithTimeout(10*time.Millisecond), + ) + + stream, _ := watcher.Watch(context.Background()) + for ev := range stream { + fmt.Println(ev) + } + + _, ok := <-stream + if ok { + t.Fatalf("unexpected opened channel") + } + +} + +func TestSortedGetter(t *testing.T) { + + firstTime := timeutils.GetKeptnTimeStamp(t0.Add(-time.Second * 2)) + secondTime := timeutils.GetKeptnTimeStamp(t0.Add(-time.Second)) + thirdTime := timeutils.GetKeptnTimeStamp(t0) + + events := []*models.KeptnContextExtendedCE{ + {Time: t0.Add(-time.Second)}, + {Time: t0}, + {Time: t0.Add(-time.Second * 2)}, + } + + SortByTime(events) + assert.Equal(t, timeutils.GetKeptnTimeStamp(events[0].Time), firstTime) + assert.Equal(t, timeutils.GetKeptnTimeStamp(events[1].Time), secondTime) + assert.Equal(t, timeutils.GetKeptnTimeStamp(events[2].Time), thirdTime) + + for _, e := range events { + fmt.Println(e.Time) + } +} diff --git a/pkg/api/utils/v2/fake/log_handler_mock.go b/pkg/api/utils/v2/fake/log_handler_mock.go new file mode 100644 index 00000000..cd068a43 --- /dev/null +++ b/pkg/api/utils/v2/fake/log_handler_mock.go @@ -0,0 +1,286 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package utils_mock + +import ( + "context" + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/api/utils/v2" + "sync" +) + +// LogsInterfaceMock is a mock implementation of v2.LogsInterface. +// +// func TestSomethingThatUsesLogsInterface(t *testing.T) { +// +// // make and configure a mocked v2.LogsInterface +// mockedLogsInterface := &LogsInterfaceMock{ +// DeleteLogsFunc: func(ctx context.Context, filter models.LogFilter, opts v2.LogsDeleteLogsOptions) error { +// panic("mock out the DeleteLogs method") +// }, +// FlushFunc: func(ctx context.Context, opts v2.LogsFlushOptions) error { +// panic("mock out the Flush method") +// }, +// GetLogsFunc: func(ctx context.Context, params models.GetLogsParams, opts v2.LogsGetLogsOptions) (*models.GetLogsResponse, error) { +// panic("mock out the GetLogs method") +// }, +// LogFunc: func(logs []models.LogEntry, opts v2.LogsLogOptions) { +// panic("mock out the Log method") +// }, +// StartFunc: func(ctx context.Context, opts v2.LogsStartOptions) { +// panic("mock out the Start method") +// }, +// } +// +// // use mockedLogsInterface in code that requires v2.LogsInterface +// // and then make assertions. +// +// } +type LogsInterfaceMock struct { + // DeleteLogsFunc mocks the DeleteLogs method. + DeleteLogsFunc func(ctx context.Context, filter models.LogFilter, opts v2.LogsDeleteLogsOptions) error + + // FlushFunc mocks the Flush method. + FlushFunc func(ctx context.Context, opts v2.LogsFlushOptions) error + + // GetLogsFunc mocks the GetLogs method. + GetLogsFunc func(ctx context.Context, params models.GetLogsParams, opts v2.LogsGetLogsOptions) (*models.GetLogsResponse, error) + + // LogFunc mocks the Log method. + LogFunc func(logs []models.LogEntry, opts v2.LogsLogOptions) + + // StartFunc mocks the Start method. + StartFunc func(ctx context.Context, opts v2.LogsStartOptions) + + // calls tracks calls to the methods. + calls struct { + // DeleteLogs holds details about calls to the DeleteLogs method. + DeleteLogs []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Filter is the filter argument value. + Filter models.LogFilter + // Opts is the opts argument value. + Opts v2.LogsDeleteLogsOptions + } + // Flush holds details about calls to the Flush method. + Flush []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Opts is the opts argument value. + Opts v2.LogsFlushOptions + } + // GetLogs holds details about calls to the GetLogs method. + GetLogs []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Params is the params argument value. + Params models.GetLogsParams + // Opts is the opts argument value. + Opts v2.LogsGetLogsOptions + } + // Log holds details about calls to the Log method. + Log []struct { + // Logs is the logs argument value. + Logs []models.LogEntry + // Opts is the opts argument value. + Opts v2.LogsLogOptions + } + // Start holds details about calls to the Start method. + Start []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Opts is the opts argument value. + Opts v2.LogsStartOptions + } + } + lockDeleteLogs sync.RWMutex + lockFlush sync.RWMutex + lockGetLogs sync.RWMutex + lockLog sync.RWMutex + lockStart sync.RWMutex +} + +// DeleteLogs calls DeleteLogsFunc. +func (mock *LogsInterfaceMock) DeleteLogs(ctx context.Context, filter models.LogFilter, opts v2.LogsDeleteLogsOptions) error { + if mock.DeleteLogsFunc == nil { + panic("LogsInterfaceMock.DeleteLogsFunc: method is nil but LogsInterface.DeleteLogs was just called") + } + callInfo := struct { + Ctx context.Context + Filter models.LogFilter + Opts v2.LogsDeleteLogsOptions + }{ + Ctx: ctx, + Filter: filter, + Opts: opts, + } + mock.lockDeleteLogs.Lock() + mock.calls.DeleteLogs = append(mock.calls.DeleteLogs, callInfo) + mock.lockDeleteLogs.Unlock() + return mock.DeleteLogsFunc(ctx, filter, opts) +} + +// DeleteLogsCalls gets all the calls that were made to DeleteLogs. +// Check the length with: +// len(mockedLogsInterface.DeleteLogsCalls()) +func (mock *LogsInterfaceMock) DeleteLogsCalls() []struct { + Ctx context.Context + Filter models.LogFilter + Opts v2.LogsDeleteLogsOptions +} { + var calls []struct { + Ctx context.Context + Filter models.LogFilter + Opts v2.LogsDeleteLogsOptions + } + mock.lockDeleteLogs.RLock() + calls = mock.calls.DeleteLogs + mock.lockDeleteLogs.RUnlock() + return calls +} + +// Flush calls FlushFunc. +func (mock *LogsInterfaceMock) Flush(ctx context.Context, opts v2.LogsFlushOptions) error { + if mock.FlushFunc == nil { + panic("LogsInterfaceMock.FlushFunc: method is nil but LogsInterface.Flush was just called") + } + callInfo := struct { + Ctx context.Context + Opts v2.LogsFlushOptions + }{ + Ctx: ctx, + Opts: opts, + } + mock.lockFlush.Lock() + mock.calls.Flush = append(mock.calls.Flush, callInfo) + mock.lockFlush.Unlock() + return mock.FlushFunc(ctx, opts) +} + +// FlushCalls gets all the calls that were made to Flush. +// Check the length with: +// len(mockedLogsInterface.FlushCalls()) +func (mock *LogsInterfaceMock) FlushCalls() []struct { + Ctx context.Context + Opts v2.LogsFlushOptions +} { + var calls []struct { + Ctx context.Context + Opts v2.LogsFlushOptions + } + mock.lockFlush.RLock() + calls = mock.calls.Flush + mock.lockFlush.RUnlock() + return calls +} + +// GetLogs calls GetLogsFunc. +func (mock *LogsInterfaceMock) GetLogs(ctx context.Context, params models.GetLogsParams, opts v2.LogsGetLogsOptions) (*models.GetLogsResponse, error) { + if mock.GetLogsFunc == nil { + panic("LogsInterfaceMock.GetLogsFunc: method is nil but LogsInterface.GetLogs was just called") + } + callInfo := struct { + Ctx context.Context + Params models.GetLogsParams + Opts v2.LogsGetLogsOptions + }{ + Ctx: ctx, + Params: params, + Opts: opts, + } + mock.lockGetLogs.Lock() + mock.calls.GetLogs = append(mock.calls.GetLogs, callInfo) + mock.lockGetLogs.Unlock() + return mock.GetLogsFunc(ctx, params, opts) +} + +// GetLogsCalls gets all the calls that were made to GetLogs. +// Check the length with: +// len(mockedLogsInterface.GetLogsCalls()) +func (mock *LogsInterfaceMock) GetLogsCalls() []struct { + Ctx context.Context + Params models.GetLogsParams + Opts v2.LogsGetLogsOptions +} { + var calls []struct { + Ctx context.Context + Params models.GetLogsParams + Opts v2.LogsGetLogsOptions + } + mock.lockGetLogs.RLock() + calls = mock.calls.GetLogs + mock.lockGetLogs.RUnlock() + return calls +} + +// Log calls LogFunc. +func (mock *LogsInterfaceMock) Log(logs []models.LogEntry, opts v2.LogsLogOptions) { + if mock.LogFunc == nil { + panic("LogsInterfaceMock.LogFunc: method is nil but LogsInterface.Log was just called") + } + callInfo := struct { + Logs []models.LogEntry + Opts v2.LogsLogOptions + }{ + Logs: logs, + Opts: opts, + } + mock.lockLog.Lock() + mock.calls.Log = append(mock.calls.Log, callInfo) + mock.lockLog.Unlock() + mock.LogFunc(logs, opts) +} + +// LogCalls gets all the calls that were made to Log. +// Check the length with: +// len(mockedLogsInterface.LogCalls()) +func (mock *LogsInterfaceMock) LogCalls() []struct { + Logs []models.LogEntry + Opts v2.LogsLogOptions +} { + var calls []struct { + Logs []models.LogEntry + Opts v2.LogsLogOptions + } + mock.lockLog.RLock() + calls = mock.calls.Log + mock.lockLog.RUnlock() + return calls +} + +// Start calls StartFunc. +func (mock *LogsInterfaceMock) Start(ctx context.Context, opts v2.LogsStartOptions) { + if mock.StartFunc == nil { + panic("LogsInterfaceMock.StartFunc: method is nil but LogsInterface.Start was just called") + } + callInfo := struct { + Ctx context.Context + Opts v2.LogsStartOptions + }{ + Ctx: ctx, + Opts: opts, + } + mock.lockStart.Lock() + mock.calls.Start = append(mock.calls.Start, callInfo) + mock.lockStart.Unlock() + mock.StartFunc(ctx, opts) +} + +// StartCalls gets all the calls that were made to Start. +// Check the length with: +// len(mockedLogsInterface.StartCalls()) +func (mock *LogsInterfaceMock) StartCalls() []struct { + Ctx context.Context + Opts v2.LogsStartOptions +} { + var calls []struct { + Ctx context.Context + Opts v2.LogsStartOptions + } + mock.lockStart.RLock() + calls = mock.calls.Start + mock.lockStart.RUnlock() + return calls +} diff --git a/pkg/api/utils/v2/fake/secret_handler_mock.go b/pkg/api/utils/v2/fake/secret_handler_mock.go new file mode 100644 index 00000000..5e8d4f8d --- /dev/null +++ b/pkg/api/utils/v2/fake/secret_handler_mock.go @@ -0,0 +1,249 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package utils_mock + +import ( + "context" + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/api/utils/v2" + "sync" +) + +// SecretsInterfaceMock is a mock implementation of v2.SecretsInterface. +// +// func TestSomethingThatUsesSecretsInterface(t *testing.T) { +// +// // make and configure a mocked v2.SecretsInterface +// mockedSecretsInterface := &SecretsInterfaceMock{ +// CreateSecretFunc: func(ctx context.Context, secret models.Secret, opts v2.SecretsCreateSecretOptions) error { +// panic("mock out the CreateSecret method") +// }, +// DeleteSecretFunc: func(ctx context.Context, secretName string, secretScope string, opts v2.SecretsDeleteSecretOptions) error { +// panic("mock out the DeleteSecret method") +// }, +// GetSecretsFunc: func(ctx context.Context, opts v2.SecretsGetSecretsOptions) (*models.GetSecretsResponse, error) { +// panic("mock out the GetSecrets method") +// }, +// UpdateSecretFunc: func(ctx context.Context, secret models.Secret, opts v2.SecretsUpdateSecretOptions) error { +// panic("mock out the UpdateSecret method") +// }, +// } +// +// // use mockedSecretsInterface in code that requires v2.SecretsInterface +// // and then make assertions. +// +// } +type SecretsInterfaceMock struct { + // CreateSecretFunc mocks the CreateSecret method. + CreateSecretFunc func(ctx context.Context, secret models.Secret, opts v2.SecretsCreateSecretOptions) error + + // DeleteSecretFunc mocks the DeleteSecret method. + DeleteSecretFunc func(ctx context.Context, secretName string, secretScope string, opts v2.SecretsDeleteSecretOptions) error + + // GetSecretsFunc mocks the GetSecrets method. + GetSecretsFunc func(ctx context.Context, opts v2.SecretsGetSecretsOptions) (*models.GetSecretsResponse, error) + + // UpdateSecretFunc mocks the UpdateSecret method. + UpdateSecretFunc func(ctx context.Context, secret models.Secret, opts v2.SecretsUpdateSecretOptions) error + + // calls tracks calls to the methods. + calls struct { + // CreateSecret holds details about calls to the CreateSecret method. + CreateSecret []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Secret is the secret argument value. + Secret models.Secret + // Opts is the opts argument value. + Opts v2.SecretsCreateSecretOptions + } + // DeleteSecret holds details about calls to the DeleteSecret method. + DeleteSecret []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // SecretName is the secretName argument value. + SecretName string + // SecretScope is the secretScope argument value. + SecretScope string + // Opts is the opts argument value. + Opts v2.SecretsDeleteSecretOptions + } + // GetSecrets holds details about calls to the GetSecrets method. + GetSecrets []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Opts is the opts argument value. + Opts v2.SecretsGetSecretsOptions + } + // UpdateSecret holds details about calls to the UpdateSecret method. + UpdateSecret []struct { + // Ctx is the ctx argument value. + Ctx context.Context + // Secret is the secret argument value. + Secret models.Secret + // Opts is the opts argument value. + Opts v2.SecretsUpdateSecretOptions + } + } + lockCreateSecret sync.RWMutex + lockDeleteSecret sync.RWMutex + lockGetSecrets sync.RWMutex + lockUpdateSecret sync.RWMutex +} + +// CreateSecret calls CreateSecretFunc. +func (mock *SecretsInterfaceMock) CreateSecret(ctx context.Context, secret models.Secret, opts v2.SecretsCreateSecretOptions) error { + if mock.CreateSecretFunc == nil { + panic("SecretsInterfaceMock.CreateSecretFunc: method is nil but SecretsInterface.CreateSecret was just called") + } + callInfo := struct { + Ctx context.Context + Secret models.Secret + Opts v2.SecretsCreateSecretOptions + }{ + Ctx: ctx, + Secret: secret, + Opts: opts, + } + mock.lockCreateSecret.Lock() + mock.calls.CreateSecret = append(mock.calls.CreateSecret, callInfo) + mock.lockCreateSecret.Unlock() + return mock.CreateSecretFunc(ctx, secret, opts) +} + +// CreateSecretCalls gets all the calls that were made to CreateSecret. +// Check the length with: +// len(mockedSecretsInterface.CreateSecretCalls()) +func (mock *SecretsInterfaceMock) CreateSecretCalls() []struct { + Ctx context.Context + Secret models.Secret + Opts v2.SecretsCreateSecretOptions +} { + var calls []struct { + Ctx context.Context + Secret models.Secret + Opts v2.SecretsCreateSecretOptions + } + mock.lockCreateSecret.RLock() + calls = mock.calls.CreateSecret + mock.lockCreateSecret.RUnlock() + return calls +} + +// DeleteSecret calls DeleteSecretFunc. +func (mock *SecretsInterfaceMock) DeleteSecret(ctx context.Context, secretName string, secretScope string, opts v2.SecretsDeleteSecretOptions) error { + if mock.DeleteSecretFunc == nil { + panic("SecretsInterfaceMock.DeleteSecretFunc: method is nil but SecretsInterface.DeleteSecret was just called") + } + callInfo := struct { + Ctx context.Context + SecretName string + SecretScope string + Opts v2.SecretsDeleteSecretOptions + }{ + Ctx: ctx, + SecretName: secretName, + SecretScope: secretScope, + Opts: opts, + } + mock.lockDeleteSecret.Lock() + mock.calls.DeleteSecret = append(mock.calls.DeleteSecret, callInfo) + mock.lockDeleteSecret.Unlock() + return mock.DeleteSecretFunc(ctx, secretName, secretScope, opts) +} + +// DeleteSecretCalls gets all the calls that were made to DeleteSecret. +// Check the length with: +// len(mockedSecretsInterface.DeleteSecretCalls()) +func (mock *SecretsInterfaceMock) DeleteSecretCalls() []struct { + Ctx context.Context + SecretName string + SecretScope string + Opts v2.SecretsDeleteSecretOptions +} { + var calls []struct { + Ctx context.Context + SecretName string + SecretScope string + Opts v2.SecretsDeleteSecretOptions + } + mock.lockDeleteSecret.RLock() + calls = mock.calls.DeleteSecret + mock.lockDeleteSecret.RUnlock() + return calls +} + +// GetSecrets calls GetSecretsFunc. +func (mock *SecretsInterfaceMock) GetSecrets(ctx context.Context, opts v2.SecretsGetSecretsOptions) (*models.GetSecretsResponse, error) { + if mock.GetSecretsFunc == nil { + panic("SecretsInterfaceMock.GetSecretsFunc: method is nil but SecretsInterface.GetSecrets was just called") + } + callInfo := struct { + Ctx context.Context + Opts v2.SecretsGetSecretsOptions + }{ + Ctx: ctx, + Opts: opts, + } + mock.lockGetSecrets.Lock() + mock.calls.GetSecrets = append(mock.calls.GetSecrets, callInfo) + mock.lockGetSecrets.Unlock() + return mock.GetSecretsFunc(ctx, opts) +} + +// GetSecretsCalls gets all the calls that were made to GetSecrets. +// Check the length with: +// len(mockedSecretsInterface.GetSecretsCalls()) +func (mock *SecretsInterfaceMock) GetSecretsCalls() []struct { + Ctx context.Context + Opts v2.SecretsGetSecretsOptions +} { + var calls []struct { + Ctx context.Context + Opts v2.SecretsGetSecretsOptions + } + mock.lockGetSecrets.RLock() + calls = mock.calls.GetSecrets + mock.lockGetSecrets.RUnlock() + return calls +} + +// UpdateSecret calls UpdateSecretFunc. +func (mock *SecretsInterfaceMock) UpdateSecret(ctx context.Context, secret models.Secret, opts v2.SecretsUpdateSecretOptions) error { + if mock.UpdateSecretFunc == nil { + panic("SecretsInterfaceMock.UpdateSecretFunc: method is nil but SecretsInterface.UpdateSecret was just called") + } + callInfo := struct { + Ctx context.Context + Secret models.Secret + Opts v2.SecretsUpdateSecretOptions + }{ + Ctx: ctx, + Secret: secret, + Opts: opts, + } + mock.lockUpdateSecret.Lock() + mock.calls.UpdateSecret = append(mock.calls.UpdateSecret, callInfo) + mock.lockUpdateSecret.Unlock() + return mock.UpdateSecretFunc(ctx, secret, opts) +} + +// UpdateSecretCalls gets all the calls that were made to UpdateSecret. +// Check the length with: +// len(mockedSecretsInterface.UpdateSecretCalls()) +func (mock *SecretsInterfaceMock) UpdateSecretCalls() []struct { + Ctx context.Context + Secret models.Secret + Opts v2.SecretsUpdateSecretOptions +} { + var calls []struct { + Ctx context.Context + Secret models.Secret + Opts v2.SecretsUpdateSecretOptions + } + mock.lockUpdateSecret.RLock() + calls = mock.calls.UpdateSecret + mock.lockUpdateSecret.RUnlock() + return calls +} diff --git a/pkg/api/utils/v2/healthCheck.go b/pkg/api/utils/v2/healthCheck.go new file mode 100644 index 00000000..4897482b --- /dev/null +++ b/pkg/api/utils/v2/healthCheck.go @@ -0,0 +1,48 @@ +package v2 + +import ( + "encoding/json" + "fmt" + "log" + "net/http" +) + +type StatusBody struct { + Status string `json:"status"` +} + +// ToJSON converts object to JSON string +func (s *StatusBody) ToJSON() ([]byte, error) { + return json.Marshal(s) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + status := StatusBody{Status: "OK"} + + body, err := status.ToJSON() + if err != nil { + log.Println(err) + } + + w.Header().Set("content-type", "application/json") + + _, err = w.Write(body) + if err != nil { + log.Println(err) + } +} + +func RunHealthEndpoint(port string) { + + http.HandleFunc("/health", healthHandler) + err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil) + if err != nil { + log.Println(err) + } +} + +func HealthEndpointHandler(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" { + w.WriteHeader(http.StatusOK) + } +} diff --git a/pkg/api/utils/v2/logUtils.go b/pkg/api/utils/v2/logUtils.go new file mode 100644 index 00000000..c33f8d3d --- /dev/null +++ b/pkg/api/utils/v2/logUtils.go @@ -0,0 +1,218 @@ +package v2 + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/benbjohnson/clock" + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +const v1LogPath = "/v1/log" + +var defaultSyncInterval = 1 * time.Minute + +// LogsLogOptions are options for LogsInterface.Log(). +type LogsLogOptions struct{} + +// LogsFlushOptions are options for LogsInterface.Flush(). +type LogsFlushOptions struct{} + +// LogsGetLogsOptions are options for LogsInterface.GetLogs(). +type LogsGetLogsOptions struct{} + +// LogsDeleteLogsOptions are options for LogsInterface.DeleteLogs(). +type LogsDeleteLogsOptions struct{} + +// LogsStartOptions are options for LogsInterface.Start(). +type LogsStartOptions struct{} + +//go:generate moq -pkg utils_mock -skip-ensure -out ./fake/log_handler_mock.go . LogsInterface +type LogsInterface interface { + // Log appends the specified logs to the log cache. + Log(logs []models.LogEntry, opts LogsLogOptions) + + // Flush flushes the log cache. + Flush(ctx context.Context, opts LogsFlushOptions) error + + // GetLogs gets logs with the specified parameters. + GetLogs(ctx context.Context, params models.GetLogsParams, opts LogsGetLogsOptions) (*models.GetLogsResponse, error) + + // DeleteLogs deletes logs matching the specified log filter. + DeleteLogs(ctx context.Context, filter models.LogFilter, opts LogsDeleteLogsOptions) error + + Start(ctx context.Context, opts LogsStartOptions) +} + +type LogHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string + logCache []models.LogEntry + theClock clock.Clock + syncInterval time.Duration + lock sync.Mutex +} + +// NewLogHandler returns a new LogHandler +func NewLogHandler(baseURL string) *LogHandler { + return NewLogHandlerWithHTTPClient(baseURL, &http.Client{Transport: getClientTransport(nil)}) +} + +// NewLogHandlerWithHTTPClient returns a new LogHandler that uses the specified http.Client +func NewLogHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *LogHandler { + return createLogHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedLogHandler returns a new LogHandler that authenticates at the endpoint via the provided token +func NewAuthenticatedLogHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *LogHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createLogHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createLogHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *LogHandler { + return &LogHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + logCache: []models.LogEntry{}, + theClock: clock.New(), + syncInterval: defaultSyncInterval, + } +} + +func (lh *LogHandler) getBaseURL() string { + return lh.baseURL +} + +func (lh *LogHandler) getAuthToken() string { + return lh.authToken +} + +func (lh *LogHandler) getAuthHeader() string { + return lh.authHeader +} + +func (lh *LogHandler) getHTTPClient() *http.Client { + return lh.httpClient +} + +// Log appends the specified logs to the log cache. +func (lh *LogHandler) Log(logs []models.LogEntry, opts LogsLogOptions) { + lh.lock.Lock() + defer lh.lock.Unlock() + lh.logCache = append(lh.logCache, logs...) +} + +// GetLogs gets logs with the specified parameters. +func (lh *LogHandler) GetLogs(ctx context.Context, params models.GetLogsParams, opts LogsGetLogsOptions) (*models.GetLogsResponse, error) { + u, err := url.Parse(lh.scheme + "://" + lh.getBaseURL() + v1LogPath) + if err != nil { + log.Fatal("error parsing url") + } + + query := u.Query() + + if params.IntegrationID != "" { + query.Set("integrationId", params.IntegrationID) + } + if params.PageSize != 0 { + query.Set("pageSize", fmt.Sprintf("%d", params.PageSize)) + } + if params.FromTime != "" { + query.Set("fromTime", params.FromTime) + } + if params.BeforeTime != "" { + query.Set("beforeTime", params.BeforeTime) + } + + u.RawQuery = query.Encode() + + body, mErr := getAndExpectOK(ctx, u.String(), lh) + if mErr != nil { + return nil, mErr.ToError() + } + + received := &models.GetLogsResponse{} + if err := received.FromJSON(body); err != nil { + return nil, err + } + + return received, nil +} + +// DeleteLogs deletes logs matching the specified log filter. +func (lh *LogHandler) DeleteLogs(ctx context.Context, params models.LogFilter, opts LogsDeleteLogsOptions) error { + u, err := url.Parse(lh.scheme + "://" + lh.getBaseURL() + v1LogPath) + if err != nil { + log.Fatal("error parsing url") + } + + query := u.Query() + + if params.IntegrationID != "" { + query.Set("integrationId", params.IntegrationID) + } + if params.FromTime != "" { + query.Set("fromTime", params.FromTime) + } + if params.BeforeTime != "" { + query.Set("beforeTime", params.BeforeTime) + } + if _, err := delete(ctx, u.String(), lh); err != nil { + return errors.New(err.GetMessage()) + } + return nil +} + +func (lh *LogHandler) Start(ctx context.Context, opts LogsStartOptions) { + ticker := lh.theClock.Ticker(lh.syncInterval) + go func() { + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lh.Flush(ctx, LogsFlushOptions{}) + } + } + }() +} + +// Flush flushes the log cache. +func (lh *LogHandler) Flush(ctx context.Context, opts LogsFlushOptions) error { + lh.lock.Lock() + defer lh.lock.Unlock() + if len(lh.logCache) == 0 { + // only send a request if we actually have some logs to send + return nil + } + createLogsPayload := &models.CreateLogsRequest{ + Logs: lh.logCache, + } + bodyStr, err := createLogsPayload.ToJSON() + if err != nil { + return err + } + if _, err := post(ctx, lh.scheme+"://"+lh.getBaseURL()+v1LogPath, bodyStr, lh); err != nil { + return errors.New(err.GetMessage()) + } + lh.logCache = []models.LogEntry{} + return nil +} diff --git a/pkg/api/utils/logUtils_test.go b/pkg/api/utils/v2/logUtils_test.go similarity index 91% rename from pkg/api/utils/logUtils_test.go rename to pkg/api/utils/v2/logUtils_test.go index 52819c9e..05330a3a 100644 --- a/pkg/api/utils/logUtils_test.go +++ b/pkg/api/utils/v2/logUtils_test.go @@ -1,16 +1,17 @@ -package api +package v2 import ( "context" "errors" - "github.com/benbjohnson/clock" - "github.com/keptn/go-utils/pkg/api/models" - "github.com/stretchr/testify/require" "net/http" "net/http/httptest" "sync" "testing" "time" + + "github.com/benbjohnson/clock" + "github.com/keptn/go-utils/pkg/api/models" + "github.com/stretchr/testify/require" ) func getTestHTTPServer(handlerFunc func(writer http.ResponseWriter, request *http.Request)) *httptest.Server { @@ -60,7 +61,7 @@ func TestLogHandler_DeleteLogs(t *testing.T) { lh := NewLogHandler(ts.URL) - got := lh.DeleteLogs(tt.args.params) + got := lh.DeleteLogs(context.Background(), tt.args.params, LogsDeleteLogsOptions{}) require.Equal(t, tt.want, got) }) } @@ -101,12 +102,12 @@ func TestLogHandler_Flush(t *testing.T) { lh := NewLogHandler(ts.URL) - lh.LogCache = []models.LogEntry{ + lh.logCache = []models.LogEntry{ { IntegrationID: "id", }, } - got := lh.Flush() + got := lh.Flush(context.Background(), LogsFlushOptions{}) if tt.wantErr { require.NotNil(t, got) } else { @@ -157,7 +158,7 @@ func TestLogHandler_GetLogs(t *testing.T) { lh := NewLogHandler(ts.URL) - got, err := lh.GetLogs(models.GetLogsParams{}) + got, err := lh.GetLogs(context.Background(), models.GetLogsParams{}, LogsGetLogsOptions{}) require.Equal(t, tt.wantErr, err) require.Equal(t, tt.want, got) }) @@ -176,13 +177,13 @@ func TestLogHandler_Log(t *testing.T) { IntegrationID: "my-id", Message: "message", }, - }) + }, LogsLogOptions{}) wg.Done() }() } wg.Wait() - require.Len(t, lh.LogCache, 100) + require.Len(t, lh.logCache, 100) } func TestLogHandler_Start(t *testing.T) { @@ -197,15 +198,15 @@ func TestLogHandler_Start(t *testing.T) { mockClock := clock.NewMock() lh := NewLogHandler(ts.URL) - lh.TheClock = mockClock + lh.theClock = mockClock lh.Log([]models.LogEntry{ { IntegrationID: "my-id", }, - }) + }, LogsLogOptions{}) - lh.Start(context.Background()) + lh.Start(context.Background(), LogsStartOptions{}) mockClock.Add(60 * time.Second) diff --git a/pkg/api/utils/v2/projectUtils.go b/pkg/api/utils/v2/projectUtils.go new file mode 100644 index 00000000..b8a79bb8 --- /dev/null +++ b/pkg/api/utils/v2/projectUtils.go @@ -0,0 +1,179 @@ +package v2 + +import ( + "context" + "crypto/tls" + "net/http" + "net/url" + "strings" + + "github.com/keptn/go-utils/pkg/common/httputils" + + "github.com/keptn/go-utils/pkg/api/models" +) + +const v1ProjectPath = "/v1/project" + +// ProjectsCreateProjectOptions are options for ProjectsInterface.CreateProject(). +type ProjectsCreateProjectOptions struct{} + +// ProjectsDeleteProjectOptions are options for ProjectsInterface.DeleteProject(). +type ProjectsDeleteProjectOptions struct{} + +// ProjectsGetProjectOptions are options for ProjectsInterface.GetProject(). +type ProjectsGetProjectOptions struct{} + +// ProjectsGetAllProjectsOptions are options for ProjectsInterface.GetAllProjects(). +type ProjectsGetAllProjectsOptions struct{} + +// ProjectsUpdateConfigurationServiceProjectOptions are options for ProjectsInterface.UpdateConfigurationServiceProject(). +type ProjectsUpdateConfigurationServiceProjectOptions struct{} + +type ProjectsInterface interface { + // CreateProject creates a new project. + CreateProject(ctx context.Context, project models.Project, opts ProjectsCreateProjectOptions) (*models.EventContext, *models.Error) + + // DeleteProject deletes a project. + DeleteProject(ctx context.Context, project models.Project, opts ProjectsDeleteProjectOptions) (*models.EventContext, *models.Error) + + // GetProject returns a project. + GetProject(ctx context.Context, project models.Project, opts ProjectsGetProjectOptions) (*models.Project, *models.Error) + + // GetAllProjects returns all projects. + GetAllProjects(ctx context.Context, opts ProjectsGetAllProjectsOptions) ([]*models.Project, error) + + // UpdateConfigurationServiceProject updates a configuration service project. + UpdateConfigurationServiceProject(ctx context.Context, project models.Project, opts ProjectsUpdateConfigurationServiceProjectOptions) (*models.EventContext, *models.Error) +} + +// ProjectHandler handles projects +type ProjectHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewProjectHandler returns a new ProjectHandler which sends all requests directly to the configuration-service +func NewProjectHandler(baseURL string) *ProjectHandler { + return NewProjectHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewProjectHandlerWithHTTPClient returns a new ProjectHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewProjectHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ProjectHandler { + return createProjectHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedProjectHandler returns a new ProjectHandler that authenticates at the api via the provided token +// and sends all requests directly to the configuration-service +func NewAuthenticatedProjectHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ProjectHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createProjectHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createProjectHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ProjectHandler { + return &ProjectHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (p *ProjectHandler) getBaseURL() string { + return p.baseURL +} + +func (p *ProjectHandler) getAuthToken() string { + return p.authToken +} + +func (p *ProjectHandler) getAuthHeader() string { + return p.authHeader +} + +func (p *ProjectHandler) getHTTPClient() *http.Client { + return p.httpClient +} + +// CreateProject creates a new project. +func (p *ProjectHandler) CreateProject(ctx context.Context, project models.Project, opts ProjectsCreateProjectOptions) (*models.EventContext, *models.Error) { + bodyStr, err := project.ToJSON() + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + return postWithEventContext(ctx, p.scheme+"://"+p.getBaseURL()+v1ProjectPath, bodyStr, p) +} + +// DeleteProject deletes a project. +func (p *ProjectHandler) DeleteProject(ctx context.Context, project models.Project, opts ProjectsDeleteProjectOptions) (*models.EventContext, *models.Error) { + return deleteWithEventContext(ctx, p.scheme+"://"+p.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, p) +} + +// GetProject returns a project. +func (p *ProjectHandler) GetProject(ctx context.Context, project models.Project, opts ProjectsGetProjectOptions) (*models.Project, *models.Error) { + body, mErr := getAndExpectSuccess(ctx, p.scheme+"://"+p.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, p) + if mErr != nil { + return nil, mErr + } + + respProject := &models.Project{} + if err := respProject.FromJSON(body); err != nil { + return nil, buildErrorResponse(err.Error()) + } + + return respProject, nil +} + +// GetAllProjects returns all projects. +func (p *ProjectHandler) GetAllProjects(ctx context.Context, opts ProjectsGetAllProjectsOptions) ([]*models.Project, error) { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + projects := []*models.Project{} + + nextPageKey := "" + + for { + url, err := url.Parse(p.scheme + "://" + p.getBaseURL() + v1ProjectPath) + if err != nil { + return nil, err + } + q := url.Query() + if nextPageKey != "" { + q.Set("nextPageKey", nextPageKey) + url.RawQuery = q.Encode() + } + + body, mErr := getAndExpectOK(ctx, url.String(), p) + if mErr != nil { + return nil, mErr.ToError() + } + + received := &models.Projects{} + if err = received.FromJSON(body); err != nil { + return nil, err + } + projects = append(projects, received.Projects...) + + if received.NextPageKey == "" || received.NextPageKey == "0" { + break + } + nextPageKey = received.NextPageKey + } + + return projects, nil +} + +// UpdateConfigurationServiceProject updates a configuration service project. +func (p *ProjectHandler) UpdateConfigurationServiceProject(ctx context.Context, project models.Project, opts ProjectsUpdateConfigurationServiceProjectOptions) (*models.EventContext, *models.Error) { + bodyStr, err := project.ToJSON() + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + return putWithEventContext(ctx, p.scheme+"://"+p.getBaseURL()+v1ProjectPath+"/"+project.ProjectName, bodyStr, p) +} diff --git a/pkg/api/utils/v2/resourceUtils.go b/pkg/api/utils/v2/resourceUtils.go new file mode 100644 index 00000000..cb3b8e22 --- /dev/null +++ b/pkg/api/utils/v2/resourceUtils.go @@ -0,0 +1,534 @@ +package v2 + +import ( + "bytes" + "context" + "crypto/tls" + b64 "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +const pathToResource = "/resource" +const pathToService = "/service" +const pathToStage = "/stage" +const configurationServiceBaseURL = "configuration-service" + +var ResourceNotFoundError = errors.New("Resource not found") + +// ResourcesCreateResourcesOptions are options for ResourcesInterface.CreateResources(). +type ResourcesCreateResourcesOptions struct{} + +// ResourcesCreateProjectResourcesOptions are options for ResourcesInterface.CreateProjectResources(). +type ResourcesCreateProjectResourcesOptions struct{} + +// ResourcesUpdateProjectResourcesOptions are options for ResourcesInterface.UpdateProjectResources(). +type ResourcesUpdateProjectResourcesOptions struct{} + +// ResourcesUpdateServiceResourcesOptions are options for ResourcesInterface.UpdateServiceResources(). +type ResourcesUpdateServiceResourcesOptions struct{} + +// ResourcesGetAllStageResourcesOptions are options for ResourcesInterface.GetAllStageResources(). +type ResourcesGetAllStageResourcesOptions struct{} + +// ResourcesGetAllServiceResourcesOptions are options for ResourcesInterface.GetAllServiceResources(). +type ResourcesGetAllServiceResourcesOptions struct{} + +// ResourcesGetResourceOptions are options for ResourcesInterface.GetResource(). +type ResourcesGetResourceOptions struct { + // URIOptions modify the resource's URI. + URIOptions []URIOption +} + +// ResourcesDeleteResourceOptions are options for ResourcesInterface.DeleteResource(). +type ResourcesDeleteResourceOptions struct { + // URIOptions modify the resource's URI. + URIOptions []URIOption +} + +// ResourcesUpdateResourceOptions are options for ResourcesInterface.UpdateResource(). +type ResourcesUpdateResourceOptions struct { + // URIOptions modify the resource's URI. + URIOptions []URIOption +} + +// ResourcesCreateResourceOptions are options for ResourcesInterface.CreateResource(). +type ResourcesCreateResourceOptions struct { + // URIOptions modify the resource's URI. + URIOptions []URIOption +} + +type ResourcesInterface interface { + // CreateResources creates a resource for the specified entity. + CreateResources(ctx context.Context, project string, stage string, service string, resources []*models.Resource, opts ResourcesCreateResourcesOptions) (*models.EventContext, *models.Error) + + // CreateProjectResources creates multiple project resources. + CreateProjectResources(ctx context.Context, project string, resources []*models.Resource, opts ResourcesCreateProjectResourcesOptions) (string, error) + + // UpdateProjectResources updates multiple project resources. + UpdateProjectResources(ctx context.Context, project string, resources []*models.Resource, opts ResourcesUpdateProjectResourcesOptions) (string, error) + + // UpdateServiceResources updates multiple service resources. + UpdateServiceResources(ctx context.Context, project string, stage string, service string, resources []*models.Resource, opts ResourcesUpdateServiceResourcesOptions) (string, error) + + // GetAllStageResources returns a list of all resources. + GetAllStageResources(ctx context.Context, project string, stage string, opts ResourcesGetAllStageResourcesOptions) ([]*models.Resource, error) + + // GetAllServiceResources returns a list of all resources. + GetAllServiceResources(ctx context.Context, project string, stage string, service string, opts ResourcesGetAllServiceResourcesOptions) ([]*models.Resource, error) + + // GetResource returns a resource from the defined ResourceScope. + GetResource(ctx context.Context, scope ResourceScope, opts ResourcesGetResourceOptions) (*models.Resource, error) + + // DeleteResource delete a resource from the URI defined by ResourceScope. + DeleteResource(ctx context.Context, scope ResourceScope, opts ResourcesDeleteResourceOptions) error + + // UpdateResource updates a resource from the URI defined by ResourceScope. + UpdateResource(ctx context.Context, resource *models.Resource, scope ResourceScope, opts ResourcesUpdateResourceOptions) (string, error) + + // CreateResource creates one or more resources at the URI defined by ResourceScope. + CreateResource(ctx context.Context, resource []*models.Resource, scope ResourceScope, opts ResourcesCreateResourceOptions) (string, error) +} + +// ResourceHandler handles resources +type ResourceHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +type resourceRequest struct { + Resources []*models.Resource `json:"resources"` +} + +// ResourceScope contains the necessary information to get a resource +type ResourceScope struct { + project string + stage string + service string + resource string +} + +// NewResourceScope returns an empty ResourceScope to fill in calling Project Stage Service or Resource functions +func NewResourceScope() *ResourceScope { + return &ResourceScope{} +} + +// Project sets the resource scope project value +func (s *ResourceScope) Project(project string) *ResourceScope { + s.project = project + return s +} + +// Stage sets the resource scope stage value +func (s *ResourceScope) Stage(stage string) *ResourceScope { + s.stage = stage + return s +} + +// Service sets the resource scope service value +func (s *ResourceScope) Service(service string) *ResourceScope { + s.service = service + return s +} + +// Resource sets the resource scope resource +func (s *ResourceScope) Resource(resource string) *ResourceScope { + s.resource = resource + return s +} + +// GetProjectPath returns a string to construct the url to path eg. //project/ +//or an empty string if the project is not set +func (s *ResourceScope) GetProjectPath() string { + return buildPath(v1ProjectPath, s.project) +} + +// GetStagePath returns a string to construct the url to a stage eg. /stage/ +//or an empty string if the stage is unset +func (s *ResourceScope) GetStagePath() string { + return buildPath(pathToStage, s.stage) +} + +// GetServicePath returns a string to construct the url to a service eg. /service/ +//or an empty string if the service is unset +func (s *ResourceScope) GetServicePath() string { + return buildPath(pathToService, url.QueryEscape(s.service)) +} + +// GetResourcePath returns a string to construct the url to a resource eg. /resource/ +//or /resource if the resource scope is empty +func (s *ResourceScope) GetResourcePath() string { + path := pathToResource + if s.resource != "" { + path += "/" + url.QueryEscape(s.resource) + } + return path +} + +func (r *ResourceHandler) buildResourceURI(scope ResourceScope) string { + buildURI := r.scheme + "://" + r.baseURL + scope.GetProjectPath() + scope.GetStagePath() + scope.GetServicePath() + scope.GetResourcePath() + return buildURI +} + +// URIOption returns a function that modifies an url +type URIOption func(url string) string + +// AppendQuery returns an option function that can modify an URI by appending a map of url query values +func AppendQuery(queryParams url.Values) URIOption { + return func(buildURI string) string { + if queryParams != nil { + buildURI = buildURI + "?" + queryParams.Encode() + } + return buildURI + } +} + +func (r *ResourceHandler) applyOptions(buildURI string, options []URIOption) string { + for _, option := range options { + buildURI = option(buildURI) + } + return buildURI +} + +// ToJSON converts object to JSON string +func (r *resourceRequest) ToJSON() ([]byte, error) { + return json.Marshal(r) +} + +func (r *resourceRequest) FromJSON(b []byte) error { + var res resourceRequest + if err := json.Unmarshal(b, &res); err != nil { + return err + } + *r = res + return nil +} + +// NewResourceHandler returns a new ResourceHandler which sends all requests directly to the configuration-service +func NewResourceHandler(baseURL string) *ResourceHandler { + return NewResourceHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewResourceHandlerWithHTTPClient returns a new ResourceHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewResourceHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ResourceHandler { + return createResourceHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedResourceHandler returns a new ResourceHandler that authenticates at the api via the provided token +// and sends all requests directly to the configuration-service +func NewAuthenticatedResourceHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ResourceHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, configurationServiceBaseURL) { + baseURL += "/" + configurationServiceBaseURL + } + + return createResourceHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createResourceHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ResourceHandler { + return &ResourceHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (r *ResourceHandler) getBaseURL() string { + return r.baseURL +} + +func (r *ResourceHandler) getAuthToken() string { + return r.authToken +} + +func (r *ResourceHandler) getAuthHeader() string { + return r.authHeader +} + +func (r *ResourceHandler) getHTTPClient() *http.Client { + return r.httpClient +} + +// CreateResources creates a resource for the specified entity. +func (r *ResourceHandler) CreateResources(ctx context.Context, project string, stage string, service string, resources []*models.Resource, opts ResourcesCreateResourcesOptions) (*models.EventContext, *models.Error) { + copiedResources := make([]*models.Resource, len(resources), len(resources)) + for i, val := range resources { + resourceContent := b64.StdEncoding.EncodeToString([]byte(val.ResourceContent)) + copiedResources[i] = &models.Resource{ResourceURI: val.ResourceURI, ResourceContent: resourceContent} + } + + resReq := &resourceRequest{ + Resources: copiedResources, + } + requestStr, err := resReq.ToJSON() + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + + if project != "" && stage != "" && service != "" { + return postWithEventContext(ctx, r.scheme+"://"+r.baseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+service+pathToResource, requestStr, r) + } else if project != "" && stage != "" && service == "" { + return postWithEventContext(ctx, r.scheme+"://"+r.baseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToResource, requestStr, r) + } else { + return postWithEventContext(ctx, r.scheme+"://"+r.baseURL+v1ProjectPath+"/"+project+"/"+pathToResource, requestStr, r) + } +} + +// CreateProjectResources creates multiple project resources. +func (r *ResourceHandler) CreateProjectResources(ctx context.Context, project string, resources []*models.Resource, opts ResourcesCreateProjectResourcesOptions) (string, error) { + return r.CreateResourcesByURI(ctx, r.scheme+"://"+r.baseURL+v1ProjectPath+"/"+project+pathToResource, resources) +} + +// UpdateProjectResources updates multiple project resources. +func (r *ResourceHandler) UpdateProjectResources(ctx context.Context, project string, resources []*models.Resource, opts ResourcesUpdateProjectResourcesOptions) (string, error) { + return r.UpdateResourcesByURI(ctx, r.scheme+"://"+r.baseURL+v1ProjectPath+"/"+project+pathToResource, resources) +} + +// UpdateServiceResources updates multiple service resources. +func (r *ResourceHandler) UpdateServiceResources(ctx context.Context, project string, stage string, service string, resources []*models.Resource, opts ResourcesUpdateServiceResourcesOptions) (string, error) { + return r.UpdateResourcesByURI(ctx, r.scheme+"://"+r.baseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+url.QueryEscape(service)+pathToResource, resources) +} + +func (r *ResourceHandler) CreateResourcesByURI(ctx context.Context, uri string, resources []*models.Resource) (string, error) { + return r.writeResources(ctx, uri, "POST", resources) +} + +func (r *ResourceHandler) UpdateResourcesByURI(ctx context.Context, uri string, resources []*models.Resource) (string, error) { + return r.writeResources(ctx, uri, "PUT", resources) +} + +func (r *ResourceHandler) writeResources(ctx context.Context, uri string, method string, resources []*models.Resource) (string, error) { + + copiedResources := make([]*models.Resource, len(resources), len(resources)) + for i, val := range resources { + copiedResources[i] = &models.Resource{ResourceURI: val.ResourceURI, ResourceContent: b64.StdEncoding.EncodeToString([]byte(val.ResourceContent))} + } + resReq := &resourceRequest{ + Resources: copiedResources, + } + + resourceStr, err := resReq.ToJSON() + if err != nil { + return "", err + } + req, err := http.NewRequestWithContext(ctx, method, uri, bytes.NewBuffer(resourceStr)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, r) + + resp, err := r.httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + version := &models.Version{} + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + return "", errors.New(string(body)) + } + + if err = version.FromJSON(body); err != nil { + return "", err + } + + return version.Version, nil +} + +func (r *ResourceHandler) UpdateResourceByURI(ctx context.Context, uri string, resource *models.Resource) (string, error) { + return r.writeResource(ctx, uri, "PUT", resource) +} + +func (r *ResourceHandler) writeResource(ctx context.Context, uri string, method string, resource *models.Resource) (string, error) { + + copiedResource := &models.Resource{ResourceURI: resource.ResourceURI, ResourceContent: b64.StdEncoding.EncodeToString([]byte(resource.ResourceContent))} + + resourceStr, err := copiedResource.ToJSON() + if err != nil { + return "", err + } + req, err := http.NewRequestWithContext(ctx, method, uri, bytes.NewBuffer(resourceStr)) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, r) + + resp, err := r.httpClient.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + return "", errors.New(string(body)) + } + + version := &models.Version{} + if err = version.FromJSON(body); err != nil { + return "", err + } + + return version.Version, nil +} + +// GetResource returns a resource from the defined ResourceScope. +func (r *ResourceHandler) GetResource(ctx context.Context, scope ResourceScope, opts ResourcesGetResourceOptions) (*models.Resource, error) { + buildURI := r.buildResourceURI(scope) + return r.GetResourceByURI(ctx, r.applyOptions(buildURI, opts.URIOptions)) +} + +//DeleteResource delete a resource from the URI defined by ResourceScope. +func (r *ResourceHandler) DeleteResource(ctx context.Context, scope ResourceScope, opts ResourcesDeleteResourceOptions) error { + buildURI := r.buildResourceURI(scope) + return r.DeleteResourceByURI(ctx, r.applyOptions(buildURI, opts.URIOptions)) +} + +//UpdateResource updates a resource from the URI defined by ResourceScope. +func (r *ResourceHandler) UpdateResource(ctx context.Context, resource *models.Resource, scope ResourceScope, opts ResourcesUpdateResourceOptions) (string, error) { + buildURI := r.buildResourceURI(scope) + return r.UpdateResourceByURI(ctx, r.applyOptions(buildURI, opts.URIOptions), resource) +} + +//CreateResource creates one or more resources at the URI defined by ResourceScope. +func (r *ResourceHandler) CreateResource(ctx context.Context, resource []*models.Resource, scope ResourceScope, opts ResourcesCreateResourceOptions) (string, error) { + buildURI := r.buildResourceURI(scope) + return r.CreateResourcesByURI(ctx, r.applyOptions(buildURI, opts.URIOptions), resource) +} + +func (r *ResourceHandler) GetResourceByURI(ctx context.Context, uri string) (*models.Resource, error) { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + body, statusCode, status, mErr := get(ctx, uri, r) + if mErr != nil { + return nil, mErr.ToError() + } + + if statusCode == 404 { + // need to handle this case differently (e.g. https://github.com/keptn/keptn/issues/1480) + return nil, ResourceNotFoundError + } + if !(statusCode >= 200 && statusCode < 300) { + if len(body) > 0 { + return nil, handleErrStatusCode(statusCode, body).ToError() + } + + return nil, buildErrorResponse(fmt.Sprintf("Received unexpected response: %d %s", statusCode, status)).ToError() + } + + resource := &models.Resource{} + if err := resource.FromJSON(body); err != nil { + return nil, err + } + + // decode resource content + decodedStr, err := b64.StdEncoding.DecodeString(resource.ResourceContent) + if err != nil { + return nil, err + } + resource.ResourceContent = string(decodedStr) + + return resource, nil +} + +func (r *ResourceHandler) DeleteResourceByURI(ctx context.Context, uri string) error { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + req, err := http.NewRequestWithContext(ctx, "DELETE", uri, nil) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/json") + addAuthHeader(req, r) + + resp, err := r.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + return nil +} + +// GetAllStageResources returns a list of all resources. +func (r *ResourceHandler) GetAllStageResources(ctx context.Context, project string, stage string, opts ResourcesGetAllStageResourcesOptions) ([]*models.Resource, error) { + myURL, err := url.Parse(r.scheme + "://" + r.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToResource) + if err != nil { + return nil, err + } + return r.getAllResources(ctx, myURL) +} + +// GetAllServiceResources returns a list of all resources. +func (r *ResourceHandler) GetAllServiceResources(ctx context.Context, project string, stage string, service string, opts ResourcesGetAllServiceResourcesOptions) ([]*models.Resource, error) { + myURL, err := url.Parse(r.scheme + "://" + r.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + + pathToService + "/" + service + pathToResource + "/") + if err != nil { + return nil, err + } + return r.getAllResources(ctx, myURL) +} + +func (r *ResourceHandler) getAllResources(ctx context.Context, u *url.URL) ([]*models.Resource, error) { + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + resources := []*models.Resource{} + + nextPageKey := "" + + for { + if nextPageKey != "" { + q := u.Query() + q.Set("nextPageKey", nextPageKey) + u.RawQuery = q.Encode() + } + + body, mErr := getAndExpectOK(ctx, u.String(), r) + if mErr != nil { + return nil, mErr.ToError() + } + + received := &models.Resources{} + if err := received.FromJSON(body); err != nil { + return nil, err + } + + resources = append(resources, received.Resources...) + + if received.NextPageKey == "" || received.NextPageKey == "0" { + break + } + nextPageKey = received.NextPageKey + } + + return resources, nil +} + +func buildPath(base, name string) string { + path := "" + if name != "" { + path = base + "/" + name + } + return path +} diff --git a/pkg/api/utils/resourceUtils_test.go b/pkg/api/utils/v2/resourceUtils_test.go similarity index 96% rename from pkg/api/utils/resourceUtils_test.go rename to pkg/api/utils/v2/resourceUtils_test.go index eba7424d..167cc8df 100644 --- a/pkg/api/utils/resourceUtils_test.go +++ b/pkg/api/utils/v2/resourceUtils_test.go @@ -1,8 +1,9 @@ -package api +package v2 import ( - "github.com/stretchr/testify/assert" "testing" + + "github.com/stretchr/testify/assert" ) func TestResourceHandler_buildResourceURI(t *testing.T) { @@ -66,8 +67,8 @@ func TestResourceHandler_buildResourceURI(t *testing.T) { } r := &ResourceHandler{ - BaseURL: configurationServiceBaseURL, - Scheme: scheme, + baseURL: configurationServiceBaseURL, + scheme: scheme, } for _, tt := range tests { diff --git a/pkg/api/utils/v2/secretUtils.go b/pkg/api/utils/v2/secretUtils.go new file mode 100644 index 00000000..c9fde4f5 --- /dev/null +++ b/pkg/api/utils/v2/secretUtils.go @@ -0,0 +1,146 @@ +package v2 + +import ( + "context" + "errors" + "net/http" + "strings" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +const secretServiceBaseURL = "secrets" +const v1SecretPath = "/v1/secret" + +// SecretsCreateSecretOptions are options for SecretsInterface.CreateSecret(). +type SecretsCreateSecretOptions struct{} + +// SecretsUpdateSecretOptions are options for SecretsInterface.UpdateSecret(). +type SecretsUpdateSecretOptions struct{} + +// SecretsDeleteSecretOptions are options for SecretsInterface.DeleteSecret(). +type SecretsDeleteSecretOptions struct{} + +// SecretsGetSecretsOptions are options for SecretsInterface.GetSecrets(). +type SecretsGetSecretsOptions struct{} + +//go:generate moq -pkg utils_mock -skip-ensure -out ./fake/secret_handler_mock.go . SecretsInterface +type SecretsInterface interface { + // CreateSecret creates a new secret. + CreateSecret(ctx context.Context, secret models.Secret, opts SecretsCreateSecretOptions) error + + // UpdateSecret creates a new secret. + UpdateSecret(ctx context.Context, secret models.Secret, opts SecretsUpdateSecretOptions) error + + // DeleteSecret deletes a secret. + DeleteSecret(ctx context.Context, secretName, secretScope string, opts SecretsDeleteSecretOptions) error + + // GetSecrets returns a list of created secrets. + GetSecrets(ctx context.Context, opts SecretsGetSecretsOptions) (*models.GetSecretsResponse, error) +} + +// SecretHandler handles secrets +type SecretHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewSecretHandler returns a new SecretHandler which sends all requests directly to the secret-service +func NewSecretHandler(baseURL string) *SecretHandler { + return NewSecretHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewSecretHandlerWithHTTPClient returns a new SecretHandler which sends all requests directly to the secret-service using the specified http.Client +func NewSecretHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *SecretHandler { + return createSecretHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedSecretHandler returns a new SecretHandler that authenticates at the api via the provided token +// and sends all requests directly to the secret-service +func NewAuthenticatedSecretHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *SecretHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, secretServiceBaseURL) { + baseURL += "/" + secretServiceBaseURL + } + + return createSecretHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createSecretHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *SecretHandler { + return &SecretHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (s *SecretHandler) getBaseURL() string { + return s.baseURL +} + +func (s *SecretHandler) getAuthToken() string { + return s.authToken +} + +func (s *SecretHandler) getAuthHeader() string { + return s.authHeader +} + +func (s *SecretHandler) getHTTPClient() *http.Client { + return s.httpClient +} + +// CreateSecret creates a new secret. +func (s *SecretHandler) CreateSecret(ctx context.Context, secret models.Secret, opts SecretsCreateSecretOptions) error { + body, err := secret.ToJSON() + if err != nil { + return err + } + _, errObj := post(ctx, s.scheme+"://"+s.baseURL+v1SecretPath, body, s) + if errObj != nil { + return errors.New(errObj.GetMessage()) + } + return nil +} + +// UpdateSecret creates a new secret. +func (s *SecretHandler) UpdateSecret(ctx context.Context, secret models.Secret, opts SecretsUpdateSecretOptions) error { + body, err := secret.ToJSON() + if err != nil { + return err + } + _, errObj := put(ctx, s.scheme+"://"+s.baseURL+v1SecretPath, body, s) + if errObj != nil { + return errors.New(errObj.GetMessage()) + } + return nil +} + +// DeleteSecret deletes a secret. +func (s *SecretHandler) DeleteSecret(ctx context.Context, secretName, secretScope string, opts SecretsDeleteSecretOptions) error { + _, err := delete(ctx, s.scheme+"://"+s.baseURL+v1SecretPath+"?name="+secretName+"&scope="+secretScope, s) + if err != nil { + return errors.New(err.GetMessage()) + } + return nil +} + +// GetSecrets returns a list of created secrets. +func (s *SecretHandler) GetSecrets(ctx context.Context, opts SecretsGetSecretsOptions) (*models.GetSecretsResponse, error) { + body, mErr := getAndExpectOK(ctx, s.scheme+"://"+s.baseURL+v1SecretPath, s) + if mErr != nil { + return nil, mErr.ToError() + } + + result := &models.GetSecretsResponse{} + if err := result.FromJSON(body); err != nil { + return nil, err + } + return result, nil +} diff --git a/pkg/api/utils/v2/sequenceUtils.go b/pkg/api/utils/v2/sequenceUtils.go new file mode 100644 index 00000000..2a23779d --- /dev/null +++ b/pkg/api/utils/v2/sequenceUtils.go @@ -0,0 +1,147 @@ +package v2 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/keptn/go-utils/pkg/common/httputils" +) + +const v1SequenceControlPath = "/v1/sequence/%s/%s/control" + +// SequencesControlSequenceOptions are options for SequencesInterface.ControlSequence(). +type SequencesControlSequenceOptions struct{} + +type SequencesInterface interface { + ControlSequence(ctx context.Context, params SequenceControlParams, opts SequencesControlSequenceOptions) error +} + +type SequenceControlHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +type SequenceControlParams struct { + Project string `json:"project"` + KeptnContext string `json:"keptnContext"` + Stage string `json:"stage"` + State string `json:"state"` +} + +func (s *SequenceControlParams) Validate() error { + var errMsg []string + if s.Project == "" { + errMsg = append(errMsg, "project parameter not set") + } + if s.KeptnContext == "" { + errMsg = append(errMsg, "keptn context parameter not set") + } + if s.State == "" { + errMsg = append(errMsg, "sequence state parameter not set") + } + errStr := strings.Join(errMsg, ",") + + if len(errMsg) > 0 { + return fmt.Errorf("failed to validate sequence control parameters: %s", errStr) + } + return nil +} + +type SequenceControlBody struct { + Stage string `json:"stage"` + State string `json:"state"` +} + +// Converts object to JSON string +func (s *SequenceControlBody) ToJSON() ([]byte, error) { + return json.Marshal(s) +} + +// FromJSON converts JSON string to object +func (s *SequenceControlBody) FromJSON(b []byte) error { + var res SequenceControlBody + if err := json.Unmarshal(b, &res); err != nil { + return err + } + *s = res + return nil +} + +// NewSequenceControlHandlerWithHTTPClient returns a new SequenceControlHandler +func NewSequenceControlHandler(baseURL string) *SequenceControlHandler { + return NewSequenceControlHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewSequenceControlHandlerWithHTTPClient returns a new SequenceControlHandler using the specified http.Client +func NewSequenceControlHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *SequenceControlHandler { + return createSequenceControlHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedSequenceControlHandler returns a new SequenceControlHandler that authenticates at the api via the provided token +func NewAuthenticatedSequenceControlHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *SequenceControlHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createSequenceControlHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createSequenceControlHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *SequenceControlHandler { + return &SequenceControlHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (s *SequenceControlHandler) getBaseURL() string { + return s.baseURL +} + +func (s *SequenceControlHandler) getAuthToken() string { + return s.authToken +} + +func (s *SequenceControlHandler) getAuthHeader() string { + return s.authHeader +} + +func (s *SequenceControlHandler) getHTTPClient() *http.Client { + return s.httpClient +} + +func (s *SequenceControlHandler) ControlSequence(ctx context.Context, params SequenceControlParams, opts SequencesControlSequenceOptions) error { + err := params.Validate() + if err != nil { + return err + } + + baseurl := fmt.Sprintf("%s://%s", s.scheme, s.getBaseURL()) + path := fmt.Sprintf(v1SequenceControlPath, params.Project, params.KeptnContext) + + body := SequenceControlBody{ + Stage: params.Stage, + State: params.State, + } + + payload, err := body.ToJSON() + if err != nil { + return err + } + + _, errResponse := post(ctx, baseurl+path, payload, s) + if errResponse != nil { + return fmt.Errorf(errResponse.GetMessage()) + } + + return nil +} diff --git a/pkg/api/utils/sequenceUtils_test.go b/pkg/api/utils/v2/sequenceUtils_test.go similarity index 92% rename from pkg/api/utils/sequenceUtils_test.go rename to pkg/api/utils/v2/sequenceUtils_test.go index 4c88a784..7f599a3f 100644 --- a/pkg/api/utils/sequenceUtils_test.go +++ b/pkg/api/utils/v2/sequenceUtils_test.go @@ -1,6 +1,7 @@ -package api +package v2 import ( + "context" "io" "net/http" "net/http/httptest" @@ -72,7 +73,7 @@ func TestSequenceControlHandler_ControlSequence(t *testing.T) { ts := httptest.NewServer(tt.Handler) defer ts.Close() s := NewSequenceControlHandler(ts.URL) - if err := s.ControlSequence(tt.params); (err != nil) != tt.wantErr { + if err := s.ControlSequence(context.Background(), tt.params, SequencesControlSequenceOptions{}); (err != nil) != tt.wantErr { t.Errorf("AbortSequence() error = %v, wantErr %v", err, tt.wantErr) } }) diff --git a/pkg/api/utils/v2/serviceUtils.go b/pkg/api/utils/v2/serviceUtils.go new file mode 100644 index 00000000..9b982e64 --- /dev/null +++ b/pkg/api/utils/v2/serviceUtils.go @@ -0,0 +1,170 @@ +package v2 + +import ( + "context" + "crypto/tls" + "net/http" + "net/url" + "strings" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +// ServicesCreateServiceInStageOptions are options for ServicesInterface.CreateServiceInStage(). +type ServicesCreateServiceInStageOptions struct{} + +// ServicesDeleteServiceFromStageOptions are options for ServicesInterface.DeleteServiceFromStage(). +type ServicesDeleteServiceFromStageOptions struct{} + +// ServicesGetServiceOptions are options for ServicesInterface.GetService(). +type ServicesGetServiceOptions struct{} + +// ServicesGetAllServicesOptions are options for ServicesInterface.GetAllServices(). +type ServicesGetAllServicesOptions struct{} + +type ServicesInterface interface { + + // CreateServiceInStage creates a new service. + CreateServiceInStage(ctx context.Context, project string, stage string, serviceName string, opts ServicesCreateServiceInStageOptions) (*models.EventContext, *models.Error) + + // DeleteServiceFromStage deletes a service from a stage. + DeleteServiceFromStage(ctx context.Context, project string, stage string, serviceName string, opts ServicesDeleteServiceFromStageOptions) (*models.EventContext, *models.Error) + + // GetService gets a service. + GetService(ctx context.Context, project, stage, service string, opts ServicesGetServiceOptions) (*models.Service, error) + + // GetAllServices returns a list of all services. + GetAllServices(ctx context.Context, project string, stage string, opts ServicesGetAllServicesOptions) ([]*models.Service, error) +} + +// ServiceHandler handles services +type ServiceHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewServiceHandler returns a new ServiceHandler which sends all requests directly to the configuration-service +func NewServiceHandler(baseURL string) *ServiceHandler { + return NewServiceHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewServiceHandlerWithHTTPClient returns a new ServiceHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewServiceHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ServiceHandler { + return createServiceHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedServiceHandler returns a new ServiceHandler that authenticates at the api via the provided token +// and sends all requests directly to the configuration-service +func NewAuthenticatedServiceHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ServiceHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createServiceHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createServiceHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ServiceHandler { + return &ServiceHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (s *ServiceHandler) getBaseURL() string { + return s.baseURL +} + +func (s *ServiceHandler) getAuthToken() string { + return s.authToken +} + +func (s *ServiceHandler) getAuthHeader() string { + return s.authHeader +} + +func (s *ServiceHandler) getHTTPClient() *http.Client { + return s.httpClient +} + +// CreateServiceInStage creates a new service. +func (s *ServiceHandler) CreateServiceInStage(ctx context.Context, project string, stage string, serviceName string, opts ServicesCreateServiceInStageOptions) (*models.EventContext, *models.Error) { + service := models.Service{ServiceName: serviceName} + body, err := service.ToJSON() + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + return postWithEventContext(ctx, s.scheme+"://"+s.baseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService, body, s) +} + +// DeleteServiceFromStage deletes a service from a stage. +func (s *ServiceHandler) DeleteServiceFromStage(ctx context.Context, project string, stage string, serviceName string, opts ServicesDeleteServiceFromStageOptions) (*models.EventContext, *models.Error) { + return deleteWithEventContext(ctx, s.scheme+"://"+s.baseURL+v1ProjectPath+"/"+project+pathToStage+"/"+stage+pathToService+"/"+serviceName, s) +} + +// GetService gets a service. +func (s *ServiceHandler) GetService(ctx context.Context, project, stage, service string, opts ServicesGetServiceOptions) (*models.Service, error) { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + url, err := url.Parse(s.scheme + "://" + s.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToService + "/" + service) + if err != nil { + return nil, err + } + + body, mErr := getAndExpectOK(ctx, url.String(), s) + if mErr != nil { + return nil, mErr.ToError() + } + + received := &models.Service{} + if err = received.FromJSON(body); err != nil { + return nil, err + } + return received, nil +} + +// GetAllServices returns a list of all services. +func (s *ServiceHandler) GetAllServices(ctx context.Context, project string, stage string, opts ServicesGetAllServicesOptions) ([]*models.Service, error) { + + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + services := []*models.Service{} + + nextPageKey := "" + + for { + url, err := url.Parse(s.scheme + "://" + s.getBaseURL() + v1ProjectPath + "/" + project + pathToStage + "/" + stage + pathToService) + if err != nil { + return nil, err + } + q := url.Query() + if nextPageKey != "" { + q.Set("nextPageKey", nextPageKey) + url.RawQuery = q.Encode() + } + + body, mErr := getAndExpectOK(ctx, url.String(), s) + if mErr != nil { + return nil, mErr.ToError() + } + + received := &models.Services{} + if err = received.FromJSON(body); err != nil { + return nil, err + } + services = append(services, received.Services...) + + if received.NextPageKey == "" || received.NextPageKey == "0" { + break + } + nextPageKey = received.NextPageKey + } + + return services, nil +} diff --git a/pkg/api/utils/v2/shipyardControllerUtils.go b/pkg/api/utils/v2/shipyardControllerUtils.go new file mode 100644 index 00000000..71660877 --- /dev/null +++ b/pkg/api/utils/v2/shipyardControllerUtils.go @@ -0,0 +1,135 @@ +package v2 + +import ( + "context" + "crypto/tls" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +const shipyardControllerBaseURL = "controlPlane" + +// ShipyardControlGetOpenTriggeredEventsOptions are options for ShipyardControlInterface.GetOpenTriggeredEvents(). +type ShipyardControlGetOpenTriggeredEventsOptions struct{} + +type ShipyardControlInterface interface { + // GetOpenTriggeredEvents returns all open triggered events. + GetOpenTriggeredEvents(ctx context.Context, filter EventFilter, opts ShipyardControlGetOpenTriggeredEventsOptions) ([]*models.KeptnContextExtendedCE, error) +} + +type ShipyardControllerHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewShipyardControllerHandler returns a new ShipyardControllerHandler which sends all requests directly to the configuration-service +func NewShipyardControllerHandler(baseURL string) *ShipyardControllerHandler { + return NewShipyardControllerHandlerWithHTTPClient(baseURL, &http.Client{Transport: wrapOtelTransport(getClientTransport(nil))}) +} + +// NewShipyardControllerHandlerWithHTTPClient returns a new ShipyardControllerHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewShipyardControllerHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *ShipyardControllerHandler { + return createShipyardControllerHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedShipyardControllerHandler returns a new ShipyardControllerHandler that authenticates at the api via the provided token +// and sends all requests directly to the configuration-service +func NewAuthenticatedShipyardControllerHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ShipyardControllerHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createShipyardControllerHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createShipyardControllerHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *ShipyardControllerHandler { + return &ShipyardControllerHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (s *ShipyardControllerHandler) getBaseURL() string { + return s.baseURL +} + +func (s *ShipyardControllerHandler) getAuthToken() string { + return s.authToken +} + +func (s *ShipyardControllerHandler) getAuthHeader() string { + return s.authHeader +} + +func (s *ShipyardControllerHandler) getHTTPClient() *http.Client { + return s.httpClient +} + +// GetOpenTriggeredEvents returns all open triggered events. +func (s *ShipyardControllerHandler) GetOpenTriggeredEvents(ctx context.Context, filter EventFilter, opts ShipyardControlGetOpenTriggeredEventsOptions) ([]*models.KeptnContextExtendedCE, error) { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + + events := []*models.KeptnContextExtendedCE{} + nextPageKey := "" + + for { + url, err := url.Parse(s.scheme + "://" + s.getBaseURL() + v1EventPath + "/triggered/" + filter.EventType) + + q := url.Query() + if nextPageKey != "" { + q.Set("nextPageKey", nextPageKey) + url.RawQuery = q.Encode() + } + if filter.Project != "" { + q.Set("project", filter.Project) + } + if filter.Service != "" { + q.Set("service", filter.Service) + } + if filter.Stage != "" { + q.Set("stage", filter.Stage) + } + + url.RawQuery = q.Encode() + + if err != nil { + return nil, err + } + + body, mErr := getAndExpectOK(ctx, url.String(), s) + if mErr != nil { + return nil, mErr.ToError() + } + + received := &models.Events{} + if err = received.FromJSON(body); err != nil { + return nil, err + } + events = append(events, received.Events...) + + if received.NextPageKey == "" || received.NextPageKey == "0" { + break + } + + nextPageKeyInt, _ := strconv.Atoi(received.NextPageKey) + + if filter.NumberOfPages > 0 && nextPageKeyInt >= filter.NumberOfPages { + break + } + + nextPageKey = received.NextPageKey + } + return events, nil +} diff --git a/pkg/api/utils/v2/sleep.go b/pkg/api/utils/v2/sleep.go new file mode 100644 index 00000000..a02a0a51 --- /dev/null +++ b/pkg/api/utils/v2/sleep.go @@ -0,0 +1,43 @@ +package v2 + +import "time" + +// Sleeper defines the interface to sleep +type Sleeper interface { + Sleep() +} + +// ConfigurableSleeper is an implementation of a sleeper +// that can be configured to sleep for a specific duration +type ConfigurableSleeper struct { + duration time.Duration + sleep func(time.Duration) +} + +// Sleep pauses the execution +func (c *ConfigurableSleeper) Sleep() { + c.sleep(c.duration) +} + +// NewConfigurableSleeper creates a new instance of a configurable sleeper which will pause execution +// of the current thread for a given duration +func NewConfigurableSleeper(duration time.Duration) *ConfigurableSleeper { + return &ConfigurableSleeper{ + duration: duration, + sleep: time.Sleep, + } +} + +// FakeSleeper is a sleeper that does not sleep +type FakeSleeper struct { +} + +// Sleep does nothing, not even sleep +func (f *FakeSleeper) Sleep() { + // no-op +} + +// NewFakeSleeper creates a new instance of a FakeSleeper +func NewFakeSleeper() *FakeSleeper { + return &FakeSleeper{} +} diff --git a/pkg/api/utils/v2/sleep_test.go b/pkg/api/utils/v2/sleep_test.go new file mode 100644 index 00000000..525eb54a --- /dev/null +++ b/pkg/api/utils/v2/sleep_test.go @@ -0,0 +1,25 @@ +package v2 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestConfigurableSleeper(t *testing.T) { + timeToSleep := 5 * time.Second + + spyTime := &SpyTime{} + sleeper := ConfigurableSleeper{timeToSleep, spyTime.Sleep} + sleeper.Sleep() + assert.Equal(t, timeToSleep, spyTime.durationSlept) +} + +type SpyTime struct { + durationSlept time.Duration +} + +func (s *SpyTime) Sleep(duration time.Duration) { + s.durationSlept = duration +} diff --git a/pkg/api/utils/v2/stageUtils.go b/pkg/api/utils/v2/stageUtils.go new file mode 100644 index 00000000..66add1f1 --- /dev/null +++ b/pkg/api/utils/v2/stageUtils.go @@ -0,0 +1,130 @@ +package v2 + +import ( + "context" + "crypto/tls" + "net/http" + "net/url" + "strings" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" +) + +// StagesCreateStageOptions are options for StagesInterface.CreateStage(). +type StagesCreateStageOptions struct{} + +// StagesGetAllStagesOptions are options for StagesInterface.GetAllStages(). +type StagesGetAllStagesOptions struct{} + +type StagesInterface interface { + + // CreateStage creates a new stage with the provided name. + CreateStage(ctx context.Context, project string, stageName string, opts StagesCreateStageOptions) (*models.EventContext, *models.Error) + + // GetAllStages returns a list of all stages. + GetAllStages(ctx context.Context, project string, opts StagesGetAllStagesOptions) ([]*models.Stage, error) +} + +// StageHandler handles stages +type StageHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewStageHandler returns a new StageHandler which sends all requests directly to the configuration-service +func NewStageHandler(baseURL string) *StageHandler { + return NewStageHandlerWithHTTPClient(baseURL, &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}) +} + +// NewStageHandlerWithHTTPClient returns a new StageHandler which sends all requests directly to the configuration-service using the specified http.Client +func NewStageHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *StageHandler { + return createStageHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedStageHandler returns a new StageHandler that authenticates at the api via the provided token +// and sends all requests directly to the configuration-service +func NewAuthenticatedStageHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *StageHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createStageHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createStageHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *StageHandler { + return &StageHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (s *StageHandler) getBaseURL() string { + return s.baseURL +} + +func (s *StageHandler) getAuthToken() string { + return s.authToken +} + +func (s *StageHandler) getAuthHeader() string { + return s.authHeader +} + +func (s *StageHandler) getHTTPClient() *http.Client { + return s.httpClient +} + +// CreateStage creates a new stage with the provided name. +func (s *StageHandler) CreateStage(ctx context.Context, project string, stageName string, opts StagesCreateStageOptions) (*models.EventContext, *models.Error) { + stage := models.Stage{StageName: stageName} + body, err := stage.ToJSON() + if err != nil { + return nil, buildErrorResponse(err.Error()) + } + return postWithEventContext(ctx, s.scheme+"://"+s.baseURL+v1ProjectPath+"/"+project+pathToStage, body, s) +} + +// GetAllStages returns a list of all stages. +func (s *StageHandler) GetAllStages(ctx context.Context, project string, opts StagesGetAllStagesOptions) ([]*models.Stage, error) { + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + stages := []*models.Stage{} + + nextPageKey := "" + for { + url, err := url.Parse(s.scheme + "://" + s.getBaseURL() + v1ProjectPath + "/" + project + pathToStage) + if err != nil { + return nil, err + } + q := url.Query() + if nextPageKey != "" { + q.Set("nextPageKey", nextPageKey) + url.RawQuery = q.Encode() + } + + body, mErr := getAndExpectOK(ctx, url.String(), s) + if mErr != nil { + return nil, mErr.ToError() + } + + received := &models.Stages{} + if err = received.FromJSON(body); err != nil { + return nil, err + } + stages = append(stages, received.Stages...) + + if received.NextPageKey == "" || received.NextPageKey == "0" { + break + } + nextPageKey = received.NextPageKey + } + return stages, nil +} diff --git a/pkg/api/utils/v2/uniformUtils.go b/pkg/api/utils/v2/uniformUtils.go new file mode 100644 index 00000000..04cee942 --- /dev/null +++ b/pkg/api/utils/v2/uniformUtils.go @@ -0,0 +1,177 @@ +package v2 + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + "github.com/keptn/go-utils/pkg/api/models" + "github.com/keptn/go-utils/pkg/common/httputils" +) + +const uniformRegistrationBaseURL = "uniform/registration" +const v1UniformPath = "/v1/uniform/registration" + +// UniformPingOptions are options for UniformInterface.Ping(). +type UniformPingOptions struct{} + +// UniformRegisterIntegrationOptions are options for UniformInterface.RegisterIntegration(). +type UniformRegisterIntegrationOptions struct{} + +// UniformCreateSubscriptionOptions are options for UniformInterface.CreateSubscription(). +type UniformCreateSubscriptionOptions struct{} + +// UniformUnregisterIntegrationOptions are options for UniformInterface.UnregisterIntegration(). +type UniformUnregisterIntegrationOptions struct{} + +// UniformGetRegistrationsOptions are options for UniformInterface.GetRegistrations(). +type UniformGetRegistrationsOptions struct{} + +type UniformInterface interface { + Ping(ctx context.Context, integrationID string, opts UniformPingOptions) (*models.Integration, error) + RegisterIntegration(ctx context.Context, integration models.Integration, opts UniformRegisterIntegrationOptions) (string, error) + CreateSubscription(ctx context.Context, integrationID string, subscription models.EventSubscription, opts UniformCreateSubscriptionOptions) (string, error) + UnregisterIntegration(ctx context.Context, integrationID string, opts UniformUnregisterIntegrationOptions) error + GetRegistrations(ctx context.Context, opts UniformGetRegistrationsOptions) ([]*models.Integration, error) +} + +type UniformHandler struct { + baseURL string + authToken string + authHeader string + httpClient *http.Client + scheme string +} + +// NewUniformHandler returns a new UniformHandler +func NewUniformHandler(baseURL string) *UniformHandler { + return NewUniformHandlerWithHTTPClient(baseURL, &http.Client{Transport: getClientTransport(nil)}) +} + +// NewUniformHandlerWithHTTPClient returns a new UniformHandler using the specified http.Client +func NewUniformHandlerWithHTTPClient(baseURL string, httpClient *http.Client) *UniformHandler { + return createUniformHandler(baseURL, "", "", httpClient, "http") +} + +// NewAuthenticatedUniformHandler returns a new UniformHandler that authenticates at the api via the provided token +func NewAuthenticatedUniformHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *UniformHandler { + baseURL = strings.TrimRight(baseURL, "/") + if !strings.HasSuffix(baseURL, shipyardControllerBaseURL) { + baseURL += "/" + shipyardControllerBaseURL + } + + return createUniformHandler(baseURL, authToken, authHeader, httpClient, scheme) +} + +func createUniformHandler(baseURL string, authToken string, authHeader string, httpClient *http.Client, scheme string) *UniformHandler { + return &UniformHandler{ + baseURL: httputils.TrimHTTPScheme(baseURL), + authHeader: authHeader, + authToken: authToken, + httpClient: httpClient, + scheme: scheme, + } +} + +func (u *UniformHandler) getBaseURL() string { + return u.baseURL +} + +func (u *UniformHandler) getAuthToken() string { + return u.authToken +} + +func (u *UniformHandler) getAuthHeader() string { + return u.authHeader +} + +func (u *UniformHandler) getHTTPClient() *http.Client { + return u.httpClient +} + +func (u *UniformHandler) Ping(ctx context.Context, integrationID string, opts UniformPingOptions) (*models.Integration, error) { + if integrationID == "" { + return nil, errors.New("could not ping an invalid IntegrationID") + } + + resp, err := put(ctx, u.scheme+"://"+u.getBaseURL()+v1UniformPath+"/"+integrationID+"/ping", nil, u) + if err != nil { + return nil, errors.New(err.GetMessage()) + } + + response := &models.Integration{} + if err := response.FromJSON([]byte(resp)); err != nil { + return nil, err + } + + return response, nil +} + +func (u *UniformHandler) RegisterIntegration(ctx context.Context, integration models.Integration, opts UniformRegisterIntegrationOptions) (string, error) { + bodyStr, err := integration.ToJSON() + if err != nil { + return "", err + } + + resp, errResponse := post(ctx, u.scheme+"://"+u.getBaseURL()+v1UniformPath, bodyStr, u) + if errResponse != nil { + return "", fmt.Errorf(errResponse.GetMessage()) + } + + registerIntegrationResponse := &models.RegisterIntegrationResponse{} + if err := registerIntegrationResponse.FromJSON([]byte(resp)); err != nil { + return "", err + } + + return registerIntegrationResponse.ID, nil +} + +func (u *UniformHandler) CreateSubscription(ctx context.Context, integrationID string, subscription models.EventSubscription, opts UniformCreateSubscriptionOptions) (string, error) { + bodyStr, err := subscription.ToJSON() + if err != nil { + return "", err + } + resp, errResponse := post(ctx, u.scheme+"://"+u.getBaseURL()+v1UniformPath+"/"+integrationID+"/subscription", bodyStr, u) + if errResponse != nil { + return "", fmt.Errorf(errResponse.GetMessage()) + } + _ = resp + + createSubscriptionResponse := &models.CreateSubscriptionResponse{} + if err := createSubscriptionResponse.FromJSON([]byte(resp)); err != nil { + return "", err + } + + return createSubscriptionResponse.ID, nil +} + +func (u *UniformHandler) UnregisterIntegration(ctx context.Context, integrationID string, opts UniformUnregisterIntegrationOptions) error { + _, err := delete(ctx, u.scheme+"://"+u.getBaseURL()+v1UniformPath+"/"+integrationID, u) + if err != nil { + return fmt.Errorf(err.GetMessage()) + } + return nil +} + +func (u *UniformHandler) GetRegistrations(ctx context.Context, opts UniformGetRegistrationsOptions) ([]*models.Integration, error) { + url, err := url.Parse(u.scheme + "://" + u.getBaseURL() + v1UniformPath) + if err != nil { + return nil, err + } + + body, mErr := getAndExpectOK(ctx, url.String(), u) + if mErr != nil { + return nil, mErr.ToError() + } + + var received []*models.Integration + err = json.Unmarshal(body, &received) + if err != nil { + return nil, err + } + return received, nil +} diff --git a/pkg/lib/v0_2_0/keptn.go b/pkg/lib/v0_2_0/keptn.go index ecf5bbb0..be87265f 100644 --- a/pkg/lib/v0_2_0/keptn.go +++ b/pkg/lib/v0_2_0/keptn.go @@ -129,7 +129,7 @@ func (k *Keptn) SendTaskStatusChangedEvent(data keptn.EventProperties, source st return k.sendEventWithBaseEventContext(data, source, err, outEventType) } -// SendTaskStartedEvent sends a .finished event for the incoming .triggered event the KeptnHandler was initialized with. +// SendTaskFinishedEvent sends a .finished event for the incoming .triggered event the KeptnHandler was initialized with. // It returns the ID of the sent CloudEvent or an error func (k *Keptn) SendTaskFinishedEvent(data keptn.EventProperties, source string) (string, error) { if k.CloudEvent == nil {